improvement: support the datalayer selecting fields in reads

This commit is contained in:
Zach Daniel 2021-04-09 00:08:11 -04:00
parent 05c933215c
commit ec57f363ed
7 changed files with 80 additions and 3 deletions

View file

@ -4,6 +4,7 @@ locals_without_parens = [
accept: 1,
action: 1,
allow_nil?: 1,
always_select?: 1,
args: 1,
argument: 2,
argument: 3,

View file

@ -23,7 +23,7 @@ defmodule Ash.Actions.Helpers do
resource
|> Ash.Resource.Info.attributes()
|> Enum.flat_map(fn attribute ->
if attribute.private? || attribute.primary_key? || attribute.name in select do
if attribute.always_select? || attribute.primary_key? || attribute.name in select do
[]
else
[attribute.name]
@ -34,4 +34,22 @@ defmodule Ash.Actions.Helpers do
end)
|> Ash.Resource.Info.put_metadata(:selected, select)
end
def attributes_to_select(%{select: nil, resource: resource}) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.map(& &1.name)
end
def attributes_to_select(%{select: select, resource: resource}) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.flat_map(fn attribute ->
if attribute.always_select? || attribute.primary_key? || attribute.name in select do
[]
else
[attribute.name]
end
end)
end
end

View file

@ -320,6 +320,12 @@ defmodule Ash.Actions.Read do
{:ok, query} <- query,
{:ok, filter} <-
filter_with_related(relationship_filter_paths, ash_query, data),
{:ok, query} <-
Ash.DataLayer.select(
query,
Helpers.attributes_to_select(ash_query),
ash_query.resource
),
{:ok, query} <-
add_aggregates(
query,
@ -575,7 +581,7 @@ defmodule Ash.Actions.Read do
} = data ->
query =
initial_query
|> Ash.Query.unset([:filter, :aggregates, :sort, :limit, :offset])
|> Ash.Query.unset([:filter, :aggregates, :sort, :limit, :offset, :select])
|> Ash.Query.limit(initial_limit)
|> Ash.Query.offset(initial_offset)
|> Ash.Query.filter(^auth_filter)

View file

@ -207,6 +207,9 @@ defmodule Ash.Changeset do
if the source field is not selected on the query/provided data an error will be produced. If loading
a relationship with a query, an error is produced if the query does not select the destination field
of the relationship.
Datalayers currently are not notified of the `select` for a changeset(unlike queries), and creates/updates select all fields when they are performed.
A select provided on a changeset simply sets the unselected fields to `nil` before returning the result.
"""
def select(changeset, fields, opts \\ []) do
if opts[:replace?] do

View file

@ -20,6 +20,7 @@ defmodule Ash.DataLayer do
| {:join, Ash.Resource.t()}
| {:aggregate, Ash.Query.Aggregate.kind()}
| {:query_aggregate, Ash.Query.Aggregate.kind()}
| :select
| :aggregate_filter
| :aggregate_sort
| :boolean_filter
@ -57,6 +58,11 @@ defmodule Ash.DataLayer do
offset :: non_neg_integer(),
resource :: Ash.Resource.t()
) :: {:ok, data_layer_query()} | {:error, term}
@callback select(
data_layer_query(),
select :: list(atom),
resource :: Ash.Resource.t()
) :: {:ok, data_layer_query()} | {:error, term}
@callback set_tenant(Ash.Resource.t(), data_layer_query(), term) ::
{:ok, data_layer_query()} | {:error, term}
@callback resource_to_query(Ash.Resource.t(), Ash.Api.t()) :: data_layer_query()
@ -121,6 +127,7 @@ defmodule Ash.DataLayer do
destroy: 2,
filter: 3,
sort: 3,
select: 3,
limit: 3,
offset: 3,
transaction: 2,
@ -302,6 +309,19 @@ defmodule Ash.DataLayer do
end
end
@spec select(data_layer_query(), offset :: list(atom), Ash.Resource.t()) ::
{:ok, data_layer_query()} | {:error, term}
def select(query, nil, _resource), do: {:ok, query}
def select(query, select, resource) do
if can?(:select, resource) do
data_layer = Ash.DataLayer.data_layer(resource)
data_layer.select(query, select, resource)
else
{:ok, query}
end
end
@spec add_aggregate(data_layer_query(), Ash.Query.Aggregate.t(), Ash.Resource.t()) ::
{:ok, data_layer_query()} | {:error, term}
def add_aggregate(query, aggregate, resource) do

View file

@ -523,7 +523,7 @@ defmodule Ash.Query do
end
@doc """
Ensure that only the specified attributes are present in the results.
Ensure that only the specified *attributes* are present in the results.
The first call to `select/2` will replace the default behavior of selecting
all attributes. Subsequent calls to `select/2` will combine the provided

View file

@ -9,6 +9,7 @@ defmodule Ash.Resource.Attribute do
:primary_key?,
:private?,
:writable?,
:always_select?,
:default,
:update_default,
:description,
@ -50,6 +51,34 @@ defmodule Ash.Resource.Attribute do
doc:
"Whether or not the attribute value contains sensitive information, like PII. If so, it will be redacted while inspecting data."
],
always_select?: [
type: :boolean,
default: false,
doc: """
Whether or not to always select this attribute when reading from the database.
Useful if fields are used in read action preparations consistently.
A primary key attribute *cannot be deselected*, so this option will have no effect.
Generally, you should favor selecting the field that you need while running your preparation. For example:
```elixir
defmodule MyApp.QueryPreparation.Thing do
use Ash.Resource.Preparation
def prepare(query, _, _) do
query
|> Ash.Query.select(:attribute_i_need)
|> Ash.Query.after_action(fn query, results ->
{:ok, Enum.map(results, fn result ->
do_something_with_attribute_i_need(result)
end)}
end)
end
end
```
"""
],
primary_key?: [
type: :boolean,
default: false,