fix: allow api.load/2 to load calculations

improvement: add `allow_nil_input` to create actions for api layers
improvement: add `load/1` builtin change
feat: change `get?: true` interface functions to raise on `nil`
This commit is contained in:
Zach Daniel 2021-04-13 15:47:50 -04:00
parent 00608e00d7
commit e353ea49c3
8 changed files with 98 additions and 8 deletions

View file

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

View file

@ -279,7 +279,9 @@ defmodule Ash.Actions.Read do
initial_query
) do
if params[:initial_data] do
List.wrap(params[:initial_data])
Request.resolve([], fn _ ->
add_calculation_values(initial_query, params[:initial_data], initial_query.calculations)
end)
else
relationship_filter_paths =
Enum.map(filter_requests, fn request ->

View file

@ -58,7 +58,15 @@ defmodule Ash.Api.Interface do
)
if unquote(interface.get?) do
unquote(api).read_one(query, Keyword.drop(opts, [:query, :tenant]))
query
|> unquote(api).read_one(Keyword.drop(opts, [:query, :tenant]))
|> case do
{:ok, nil} ->
{:error, Ash.Error.Query.NotFound.exception(resource: query.resource)}
{:ok, result} ->
{:ok, result}
end
else
unquote(api).read(query, Keyword.drop(opts, [:query, :tenant]))
end
@ -92,7 +100,15 @@ defmodule Ash.Api.Interface do
)
if unquote(interface.get?) do
unquote(api).read_one!(query, Keyword.drop(opts, [:query, :tenant]))
query
|> unquote(api).read_one!(Keyword.drop(opts, [:query, :tenant]))
|> case do
{:ok, nil} ->
{:error, Ash.Error.Query.NotFound.exception(resource: query.resource)}
{:ok, result} ->
{:ok, result}
end
else
unquote(api).read!(query, Keyword.drop(opts, [:query, :tenant]))
end

View file

@ -7,6 +7,7 @@ defmodule Ash.Resource.Actions.Create do
accept: nil,
arguments: [],
changes: [],
allow_nil: [],
reject: [],
type: :create
]
@ -15,6 +16,7 @@ defmodule Ash.Resource.Actions.Create do
type: :create,
name: atom,
accept: [atom],
allow_nil: [atom],
arguments: [Ash.Resource.Actions.Argument.t()],
primary?: boolean,
description: String.t()
@ -25,7 +27,21 @@ defmodule Ash.Resource.Actions.Create do
@global_opts shared_options()
@create_update_opts create_update_opts()
@opt_schema []
@opt_schema [
allow_nil_input: [
type: {:list, :atom},
doc: """
A list of attributes that would normally be required, but should not be for this action.
This exists because extensions like ash_graphql and ash_json_api will add non-null validations to their input for any attribute
that is accepted by an action that has `allow_nil?: false`. This tells those extensions that some `change` on the resource might
set that attribute, and so they should not require it at the API layer.
Ash core doesn't actually use the values in this list, because it does its `nil` validation *after* running all resource
changes. If the value is still `nil` by the time Ash would submit to the data layer, then an error is returned.
"""
]
]
|> Ash.OptionsHelpers.merge_schemas(
@global_opts,
"Action Options"

View file

@ -21,15 +21,14 @@ defmodule Ash.Resource.Actions.SharedOptions do
@create_update_opts [
accept: [
type: {:custom, Ash.OptionsHelpers, :list_of_atoms, []},
doc:
"The list of attributes and relationships to accept. Defaults to all attributes on the resource"
doc: "The list of attributes to accept. Defaults to all attributes on the resource"
],
reject: [
type: {:custom, Ash.OptionsHelpers, :list_of_atoms, []},
doc: """
A list of attributes and relationships not to accept. This is useful if you want to say 'accept all but x'
A list of attributes not to accept. This is useful if you want to say 'accept all but x'
If this is specified along with `accept`, then everything in the `accept` list minuse any matches in the
If this is specified along with `accept`, then everything in the `accept` list minus any matches in the
`reject` list will be accepted.
"""
]

View file

@ -52,4 +52,11 @@ defmodule Ash.Resource.Change.Builtins do
def set_context(context) do
{Ash.Resource.Change.SetContext, context: context}
end
@doc """
Passes the provided value into `changeset.api.load()`, after the action has completed.
"""
def load(value) do
{Ash.Resource.Change.Load, target: value}
end
end

View file

@ -0,0 +1,11 @@
defmodule Ash.Resource.Change.Load do
@moduledoc false
use Ash.Resource.Change
alias Ash.Changeset
def change(changeset, opts, _) do
Changeset.after_action(changeset, fn changeset, result ->
changeset.api.load(result, opts[:target])
end)
end
end

View file

@ -0,0 +1,38 @@
defmodule Ash.Test.Resource.Changes.LoadTest do
@moduledoc false
use ExUnit.Case, async: true
defmodule Post do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets
attributes do
uuid_primary_key :id
attribute :text, :string
attribute :second_text, :string
end
actions do
create :create do
change load(:full_text)
end
end
calculations do
calculate :full_text, :string, concat([:text, :second_text])
end
end
defmodule Api do
use Ash.Api
resources do
resource Post
end
end
test "you can use it to load on create" do
assert Api.create!(Ash.Changeset.for_create(Post, :create, text: "foo", second_text: "bar")).full_text ==
"foobar"
end
end