mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
improvement: remove relationship writability, as it all happens through arguments now
improvement: repurpose `writable?` on `belongs_to` to make the attribute writable
This commit is contained in:
parent
15cd3fb7bc
commit
ac4590a0ca
14 changed files with 91 additions and 106 deletions
|
@ -351,30 +351,6 @@ defmodule Ash.Changeset do
|
|||
end
|
||||
|
||||
@for_create_opts [
|
||||
relationships: [
|
||||
type: :any,
|
||||
doc: """
|
||||
customize relationship behavior.
|
||||
|
||||
By default, any relationships are ignored. There are three ways to change relationships with this function:
|
||||
|
||||
### Action Arguments (preferred)
|
||||
|
||||
Create an argument on the action and add a `Ash.Resource.Change.Builtins.manage_relationship/3` change to the action.
|
||||
|
||||
### Overrides
|
||||
|
||||
You can pass the `relationships` option to specify the behavior. It is a keyword list of relationship and either
|
||||
* one of the preset manage types: #{inspect(@manage_types)}
|
||||
* explicit options, in the form of `{:manage, [...opts]}`
|
||||
|
||||
```elixir
|
||||
Ash.Changeset.for_create(MyResource, :create, params, relationships: [relationship: :append, other_relationship: {:manage, [...opts]}])
|
||||
```
|
||||
|
||||
You can also use explicit calls to `manage_relationship/4`.
|
||||
"""
|
||||
],
|
||||
require?: [
|
||||
type: :boolean,
|
||||
default: false,
|
||||
|
@ -532,7 +508,7 @@ defmodule Ash.Changeset do
|
|||
|> set_tenant(opts[:tenant] || changeset.tenant)
|
||||
|> Map.put(:__validated_for_action__, action.name)
|
||||
|> Map.put(:action, action)
|
||||
|> cast_params(action, params, opts)
|
||||
|> cast_params(action, params)
|
||||
|> set_argument_defaults(action)
|
||||
|> run_action_changes(action, opts[:actor])
|
||||
|> add_validations()
|
||||
|
@ -560,7 +536,7 @@ defmodule Ash.Changeset do
|
|||
|> set_tenant(opts[:tenant] || changeset.tenant || changeset.data.__metadata__[:tenant])
|
||||
|> Map.put(:action, action)
|
||||
|> Map.put(:__validated_for_action__, action.name)
|
||||
|> cast_params(action, params || %{}, opts)
|
||||
|> cast_params(action, params || %{})
|
||||
|> set_argument_defaults(action)
|
||||
|> validate_attributes_accepted(action)
|
||||
|> require_values(action.type, false, action.require_attributes)
|
||||
|
@ -767,7 +743,7 @@ defmodule Ash.Changeset do
|
|||
end
|
||||
end
|
||||
|
||||
defp cast_params(changeset, action, params, opts) do
|
||||
defp cast_params(changeset, action, params) do
|
||||
changeset = %{
|
||||
changeset
|
||||
| params: Map.merge(changeset.params || %{}, Enum.into(params || %{}, %{}))
|
||||
|
@ -785,24 +761,6 @@ defmodule Ash.Changeset do
|
|||
changeset
|
||||
end
|
||||
|
||||
rel = Ash.Resource.Info.public_relationship(changeset.resource, name) ->
|
||||
if rel.writable? do
|
||||
behaviour = opts[:relationships][rel.name]
|
||||
|
||||
case behaviour do
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
type when is_atom(type) ->
|
||||
manage_relationship(changeset, rel.name, value, type: type)
|
||||
|
||||
{:manage, manage_opts} ->
|
||||
manage_relationship(changeset, rel.name, value, manage_opts)
|
||||
end
|
||||
else
|
||||
changeset
|
||||
end
|
||||
|
||||
true ->
|
||||
changeset
|
||||
end
|
||||
|
@ -1568,11 +1526,6 @@ defmodule Ash.Changeset do
|
|||
* belongs_to - an update action on the source resource
|
||||
"""
|
||||
],
|
||||
relationships: [
|
||||
type: :any,
|
||||
default: [],
|
||||
doc: "A keyword list of instructions for nested relationships."
|
||||
],
|
||||
error_path: [
|
||||
type: :any,
|
||||
doc: """
|
||||
|
@ -1778,15 +1731,6 @@ defmodule Ash.Changeset do
|
|||
|
||||
add_error(changeset, error)
|
||||
|
||||
%{writable?: false} = relationship ->
|
||||
error =
|
||||
InvalidRelationship.exception(
|
||||
relationship: relationship.name,
|
||||
message: "Relationship is not editable"
|
||||
)
|
||||
|
||||
add_error(changeset, error)
|
||||
|
||||
relationship ->
|
||||
if relationship.cardinality == :many && is_map(input) && !is_struct(input) do
|
||||
case map_input_to_list(input) do
|
||||
|
|
|
@ -159,7 +159,18 @@ defmodule Ash.Dsl do
|
|||
|
||||
defmacro __after_compile__(_, _) do
|
||||
quote do
|
||||
Ash.Dsl.Extension.run_after_compile()
|
||||
transformers_to_run =
|
||||
@extensions
|
||||
|> Enum.flat_map(& &1.transformers())
|
||||
|> Ash.Dsl.Transformer.sort()
|
||||
|> Enum.filter(& &1.after_compile?())
|
||||
|
||||
__MODULE__
|
||||
|> Ash.Dsl.Extension.run_transformers(
|
||||
transformers_to_run,
|
||||
Module.get_attribute(__MODULE__, :ash_dsl_config),
|
||||
false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -484,23 +484,6 @@ defmodule Ash.Dsl.Extension do
|
|||
end
|
||||
end
|
||||
|
||||
defmacro run_after_compile do
|
||||
quote do
|
||||
transformers_to_run =
|
||||
@extensions
|
||||
|> Enum.flat_map(& &1.transformers())
|
||||
|> Ash.Dsl.Transformer.sort()
|
||||
|> Enum.filter(& &1.after_compile?())
|
||||
|
||||
__MODULE__
|
||||
|> Ash.Dsl.Extension.run_transformers(
|
||||
transformers_to_run,
|
||||
Module.get_attribute(__MODULE__, :ash_dsl_config),
|
||||
false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def run_transformers(mod, transformers, ash_dsl_config, store?) do
|
||||
Enum.reduce_while(transformers, ash_dsl_config, fn transformer, dsl ->
|
||||
result =
|
||||
|
|
|
@ -10,6 +10,7 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
|
|||
alias ElixirSense.Providers.Suggestion.Matcher
|
||||
|
||||
def suggestions(hint, {_, function_call, arg_index, info}, _chain, opts) do
|
||||
opts = add_module_store(opts)
|
||||
option = info.option || get_option(opts.cursor_context.text_before)
|
||||
|
||||
if option do
|
||||
|
@ -20,6 +21,7 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
|
|||
end
|
||||
|
||||
def suggestions(hint, opts) do
|
||||
opts = add_module_store(opts)
|
||||
option = get_section_option(opts.cursor_context.text_before)
|
||||
|
||||
if option do
|
||||
|
@ -29,6 +31,22 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
|
|||
end
|
||||
end
|
||||
|
||||
# For some reason, the module store does not change when modules are defined
|
||||
# so we are building our own fresh copy here. This is definitely a performance
|
||||
# hit
|
||||
defp add_module_store(opts) do
|
||||
Map.put(opts, :module_store, ElixirSense.Core.ModuleStore.build(all_loaded()))
|
||||
end
|
||||
|
||||
defp all_loaded do
|
||||
:code.all_loaded()
|
||||
|> Enum.filter(fn
|
||||
{mod, _} when is_atom(mod) -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
end
|
||||
|
||||
def get_suggestions(hint, opts, opt_path \\ [], type \\ nil) do
|
||||
with true <- Enum.any?(opts.env.attributes, &(&1.name == :ash_is)),
|
||||
dsl_mod when not is_nil(dsl_mod) <-
|
||||
|
@ -587,7 +605,14 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
|
|||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.map(&Module.concat([&1]))
|
||||
|> Enum.filter(&Code.ensure_loaded?/1)
|
||||
|> Enum.filter(fn module ->
|
||||
try do
|
||||
Code.ensure_loaded?(module)
|
||||
rescue
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,6 +30,41 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
|
|||
else
|
||||
[]
|
||||
end
|
||||
|> Enum.reject(fn
|
||||
%{name: name} when name in ["__info__", "module_info", "module_info"] ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|> Enum.map(fn
|
||||
%{type: :function, origin: origin, name: name, arity: arity} = completion ->
|
||||
try do
|
||||
{:docs_v1, _, _, _, _, _, functions} = Code.fetch_docs(Module.concat([origin]))
|
||||
|
||||
new_summary =
|
||||
Enum.find_value(functions, fn
|
||||
{{:function, func_name, func_arity}, _, _,
|
||||
%{
|
||||
"en" => docs
|
||||
}, _} ->
|
||||
if to_string(func_name) == name && func_arity == arity do
|
||||
docs
|
||||
end
|
||||
|
||||
_other ->
|
||||
false
|
||||
end)
|
||||
|
||||
%{completion | summary: new_summary || completion.summary}
|
||||
rescue
|
||||
_e ->
|
||||
completion
|
||||
end
|
||||
|
||||
other ->
|
||||
other
|
||||
end)
|
||||
|
||||
custom =
|
||||
for module <- module_store.by_behaviour[behaviour] || [],
|
||||
|
@ -51,6 +86,8 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do
|
|||
builtins ++ custom
|
||||
end
|
||||
|
||||
defp lowercase_string?(""), do: true
|
||||
|
||||
defp lowercase_string?(string) do
|
||||
first = String.first(string)
|
||||
String.downcase(first) == first
|
||||
|
|
|
@ -24,8 +24,8 @@ defmodule Ash.Resource.Change.Builtins do
|
|||
|
||||
If a zero argument function is provided, it is called to determine the value.
|
||||
|
||||
If a tuple of `{:arg, :argument_name}` is provided, the value will be read from the argument if supplied.
|
||||
If the argument is not supplied then nothing happens.
|
||||
If a `arg(:arg_name)` is provided, the value will be read from the argument if supplied.
|
||||
If the argument specified is not given to the action, then nothing happens.
|
||||
"""
|
||||
def set_attribute(attribute, value) do
|
||||
{Ash.Resource.Change.SetAttribute, attribute: attribute, value: value}
|
||||
|
|
|
@ -56,6 +56,12 @@ defmodule Ash.Resource.Relationships.BelongsTo do
|
|||
|
||||
@opt_schema Ash.OptionsHelpers.merge_schemas(
|
||||
[
|
||||
writable?: [
|
||||
type: :boolean,
|
||||
doc:
|
||||
"Whether or not the attribute created by this relationship will be marked with `writable?: true`.",
|
||||
default: false
|
||||
],
|
||||
primary_key?: [
|
||||
type: :boolean,
|
||||
default: false,
|
||||
|
|
|
@ -8,7 +8,6 @@ defmodule Ash.Resource.Relationships.HasMany do
|
|||
:source_field,
|
||||
:source,
|
||||
:context,
|
||||
:writable?,
|
||||
:description,
|
||||
:filter,
|
||||
:sort,
|
||||
|
@ -28,7 +27,6 @@ defmodule Ash.Resource.Relationships.HasMany do
|
|||
type: :has_many,
|
||||
cardinality: :many,
|
||||
source: Ash.Resource.t(),
|
||||
writable?: boolean,
|
||||
read_action: atom,
|
||||
filter: Ash.Filter.t() | nil,
|
||||
no_fields?: boolean,
|
||||
|
|
|
@ -9,7 +9,6 @@ defmodule Ash.Resource.Relationships.HasOne do
|
|||
:private?,
|
||||
:source_field,
|
||||
:allow_orphans?,
|
||||
:writable?,
|
||||
:context,
|
||||
:description,
|
||||
:filter,
|
||||
|
@ -31,7 +30,6 @@ defmodule Ash.Resource.Relationships.HasOne do
|
|||
type: :has_one,
|
||||
cardinality: :one,
|
||||
source: Ash.Resource.t(),
|
||||
writable?: boolean,
|
||||
name: atom,
|
||||
read_action: atom,
|
||||
no_fields?: boolean,
|
||||
|
|
|
@ -13,7 +13,6 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
:not_found_message,
|
||||
:violation_message,
|
||||
:api,
|
||||
:writable?,
|
||||
:private?,
|
||||
:sort,
|
||||
:read_action,
|
||||
|
@ -30,7 +29,6 @@ defmodule Ash.Resource.Relationships.ManyToMany do
|
|||
type: :many_to_many,
|
||||
cardinality: :many,
|
||||
source: Ash.Resource.t(),
|
||||
writable?: boolean,
|
||||
private?: boolean,
|
||||
filter: Ash.Filter.t() | nil,
|
||||
read_action: atom,
|
||||
|
|
|
@ -26,11 +26,6 @@ defmodule Ash.Resource.Relationships.SharedOptions do
|
|||
doc:
|
||||
"The field on this resource that should match the `destination_field` on the related resource."
|
||||
],
|
||||
writable?: [
|
||||
type: :boolean,
|
||||
doc: "Whether or not the relationship may be edited.",
|
||||
default: true
|
||||
],
|
||||
description: [
|
||||
type: :string,
|
||||
doc: "An optional description for the relationship"
|
||||
|
|
|
@ -29,7 +29,7 @@ defmodule Ash.Resource.Transformers.BelongsToAttribute do
|
|||
else
|
||||
not relationship.required?
|
||||
end,
|
||||
writable?: false,
|
||||
writable?: relationship.writable?,
|
||||
private?: true,
|
||||
primary_key?: relationship.primary_key?
|
||||
)
|
||||
|
|
|
@ -692,18 +692,6 @@ defmodule Ash.Test.Actions.CreateTest do
|
|||
end
|
||||
|
||||
describe "creating with required belongs_to relationships" do
|
||||
test "allows creating with belongs_to relationship" do
|
||||
author =
|
||||
Author
|
||||
|> new()
|
||||
|> change_attribute(:bio, "best dude")
|
||||
|> Api.create!()
|
||||
|
||||
ProfileWithBelongsTo
|
||||
|> Ash.Changeset.for_create(:create, [author: author], relationships: [author: :replace])
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "does not allow creating without the required belongs_to relationship" do
|
||||
assert_raise Ash.Error.Invalid, ~r/relationship author is required/, fn ->
|
||||
ProfileWithBelongsTo
|
||||
|
|
|
@ -543,7 +543,8 @@ defmodule Ash.ElixirSense.PluginTest do
|
|||
snippet: nil,
|
||||
spec: "",
|
||||
summary:
|
||||
"Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument",
|
||||
"Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument" <>
|
||||
_,
|
||||
type: :function,
|
||||
visibility: :public
|
||||
},
|
||||
|
@ -558,7 +559,8 @@ defmodule Ash.ElixirSense.PluginTest do
|
|||
snippet: nil,
|
||||
spec: "",
|
||||
summary:
|
||||
"Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument",
|
||||
"Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument" <>
|
||||
_,
|
||||
type: :function,
|
||||
visibility: :public
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue