From ad0af9831c596b076e0d7105a64de93b643308ee Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 28 Jun 2021 01:33:31 -0400 Subject: [PATCH] improvement: if "" fails to cast, cast it as `nil` instead improvement: ReadActionRequiresActor error improvement: `ensure_selected` change --- lib/ash/changeset/changeset.ex | 23 +++++++++++++++++++ .../error/query/read_action_requires_actor.ex | 16 +++++++++++++ lib/ash/query/query.ex | 13 ++++++++++- lib/ash/resource/change/builtins.ex | 7 ++++++ lib/ash/resource/change/select.ex | 6 ++++- lib/ash/type/type.ex | 16 +++++++++++-- 6 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 lib/ash/error/query/read_action_requires_actor.ex diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 592b486e..a354571d 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -211,6 +211,8 @@ defmodule Ash.Changeset do 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. + + Use `ensure_selected/2` if you simply wish to make sure a field has been selected, without deselecting any other fields. """ def select(changeset, fields, opts \\ []) do if opts[:replace?] do @@ -220,6 +222,27 @@ defmodule Ash.Changeset do end end + @doc """ + Ensures that the given attributes are selected. + + The first call to `select/2` will *limit* the fields to only the provided fields. + Use `ensure_selected/2` to say "select this field (or these fields) without deselecting anything else". + + See `select/2` for more. + """ + def ensure_selected(changeset, fields) do + if changeset.select do + Ash.Changeset.select(changeset, List.wrap(fields)) + else + to_select = + changeset.resource + |> Ash.Resource.Info.attributes() + |> Enum.map(& &1.name) + + Ash.Changeset.select(changeset, to_select) + end + end + @doc """ Ensure the the specified attributes are `nil` in the changeset results. """ diff --git a/lib/ash/error/query/read_action_requires_actor.ex b/lib/ash/error/query/read_action_requires_actor.ex new file mode 100644 index 00000000..80eeebc9 --- /dev/null +++ b/lib/ash/error/query/read_action_requires_actor.ex @@ -0,0 +1,16 @@ +defmodule Ash.Error.Query.ReadActionRequiresActor do + @moduledoc "Used when an actor is referenced in a filter template, but no actor exists" + use Ash.Error.Exception + + def_ash_error([], class: :invalid) + + defimpl Ash.ErrorKind do + def id(_), do: Ash.UUID.generate() + + def code(_), do: "actor_required" + + def message(_error) do + "actor is required" + end + end +end diff --git a/lib/ash/query/query.ex b/lib/ash/query/query.ex index f10a5f3d..5b9e41e0 100644 --- a/lib/ash/query/query.ex +++ b/lib/ash/query/query.ex @@ -63,6 +63,7 @@ defmodule Ash.Query do InvalidLimit, InvalidOffset, NoReadAction, + ReadActionRequiresActor, Required } @@ -367,7 +368,7 @@ defmodule Ash.Query do defp add_action_filters(query, action, actor) do if Ash.Filter.template_references_actor?(action.filter) and is_nil(actor) do - Ash.Query.add_error(query, "Read action requires actor") + Ash.Query.add_error(query, ReadActionRequiresActor.exception([])) else built_filter = Ash.Filter.build_filter_from_template( @@ -597,6 +598,8 @@ defmodule Ash.Query 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. + + Use `ensure_selected/2` if you simply wish to make sure a field has been selected, without deselecting any other fields. """ def select(query, fields, opts \\ []) do query = to_query(query) @@ -608,6 +611,14 @@ defmodule Ash.Query do end end + @doc """ + Ensures that the given attributes are selected. + + The first call to `select/2` will *limit* the fields to only the provided fields. + Use `ensure_selected/2` to say "select this field (or these fields) without deselecting anything else". + + See `select/2` for more. + """ def ensure_selected(query, fields) do if query.select do Ash.Query.select(query, List.wrap(fields)) diff --git a/lib/ash/resource/change/builtins.ex b/lib/ash/resource/change/builtins.ex index 0d5603e4..7e1c5d04 100644 --- a/lib/ash/resource/change/builtins.ex +++ b/lib/ash/resource/change/builtins.ex @@ -72,4 +72,11 @@ defmodule Ash.Resource.Change.Builtins do def select(value) do {Ash.Resource.Change.Select, target: value} end + + @doc """ + Passes the provided value into `Ash.Changeset.ensure_selected/2` + """ + def ensure_selected(value) do + {Ash.Resource.Change.Select, target: value, ensure?: true} + end end diff --git a/lib/ash/resource/change/select.ex b/lib/ash/resource/change/select.ex index 0bf19697..d0202d60 100644 --- a/lib/ash/resource/change/select.ex +++ b/lib/ash/resource/change/select.ex @@ -3,6 +3,10 @@ defmodule Ash.Resource.Change.Select do use Ash.Resource.Change def change(changeset, opts, _) do - Ash.Changeset.select(changeset, opts[:target] || []) + if opts[:ensure?] do + Ash.Changeset.ensure_selected(changeset, opts[:target] || []) + else + Ash.Changeset.select(changeset, opts[:target] || []) + end end end diff --git a/lib/ash/type/type.ex b/lib/ash/type/type.ex index 9c382fa8..6a8027f1 100644 --- a/lib/ash/type/type.ex +++ b/lib/ash/type/type.ex @@ -356,10 +356,22 @@ defmodule Ash.Type do {:ok, value} :error -> - {:error, "is invalid"} + case term do + "" -> + cast_input(type, nil, constraints) + + _ -> + {:error, "is invalid"} + end {:error, other} -> - {:error, other} + case term do + "" -> + cast_input(type, nil, constraints) + + _ -> + {:error, other} + end end end