Ash.Changeset (ash v1.37.2) View Source
Changesets are used to create and update data in Ash.
Create a changeset with new/1
or new/2
, and alter the attributes
and relationships using the functions provided in this module. Nothing in this module
actually incurs changes in a data layer. To commit a changeset, see Ash.Api.create/2
and Ash.Api.update/2
.
For example:
Ash.Changeset.replace_relationship(changeset, :linked_tickets, [
{1, %{link_type: "blocking"}},
{a_ticket, %{link_type: "caused_by"}},
{%{id: 2}, %{link_type: "related_to"}}
])
Ash.Changeset.replace_relationship/3
, Ash.Changeset.append_to_relationship/3
and Ash.Changeset.remove_from_relationship/3
are simply about managing what data is/isn't related. A simple example might be updating the tags of a post, where all the tags
already exist, we simply want to edit the information. They are shorthands for calling Ash.Changeset.manage_relationship/4
with
a specific set of options.
See the action DSL documentation for more.
Link to this section Summary
Functions
Adds an error to the changesets errors list, and marks the change as valid?: false
Adds an after_action hook to the changeset.
Appends a record or a list of records to a relationship.
Returns the original data with attribute changes merged, if the changeset is valid.
Adds a before_action hook to the changeset.
Adds a change to the changeset, unless the value matches the existing value
Calls change_attribute/3
for each key/value pair provided
Change an attribute only if is not currently being changed
Change an attribute if is not currently being changed, by calling the provided function
Returns true if an attribute exists in the changes
Returns true if a relationship exists in the changes
Clears an attribute or relationship change off of the changeset
Remove an argument from the changeset
Ensure the the specified attributes are nil
in the changeset results.
fetches the value of an argument provided to the changeset or :error
Gets the new value for an attribute, or :error
if it is not being changed
Constructs a changeset for a given create action, and validates it.
Constructs a changeset for a given destroy action, and validates it.
Constructs a changeset for a given update action, and validates it.
Changes an attribute even if it isn't writable
Calls force_change_attribute/3
for each key/value pair provided
Force change an attribute if is not currently being changed, by calling the provided function
Gets the value of an argument provided to the changeset
Gets the changing value or the original value of an attribute
Gets the original value for an attribute
Manages the related records by creating, updating, or destroying them as necessary.
Return a changeset over a resource or a record. params
can be either attributes, relationship values or arguments.
Puts a key/value in the changeset context that can be used later
Removes a record or a list of records to a relationship.
Ensure that only the specified attributes are present in the results.
Add an argument to the changeset, which will be provided to the action
Merge a map of arguments to the arguments list
Deep merges the provided map into the changeset context that can be used later
Wraps a function in the before/after action hooks of a changeset.
Link to this section Types
Specs
manage_relationship_type() :: :replace | :append | :remove | :direct_control
Specs
t() :: %Ash.Changeset{ __validated_for_action__: term(), action: term(), action_failed?: term(), action_type: term(), after_action: term(), api: term(), arguments: term(), attributes: term(), before_action: term(), change_dependencies: term(), context: term(), data: term(), errors: term(), params: term(), relationships: term(), resource: term(), select: term(), tenant: term(), valid?: term() }
Link to this section Functions
Specs
Adds an error to the changesets errors list, and marks the change as valid?: false
Specs
after_action( t(), (t(), Ash.Resource.record() -> {:ok, Ash.Resource.record()} | {:ok, Ash.Resource.record(), [Ash.Notifier.Notification.t()]} | {:error, term()}) ) :: t()
Adds an after_action hook to the changeset.
append_to_relationship(changeset, relationship, record_or_records, opts \\ [])
View SourceSpecs
append_to_relationship( t(), atom(), Ash.Resource.record() | map() | term() | [Ash.Resource.record() | map() | term()], Keyword.t() ) :: t()
Appends a record or a list of records to a relationship.
Alias for:
manage_to_relationship(changeset, relationship, input,
on_lookup: :relate, # If a record is not in the relationship, and can be found, relate it
on_no_match: :error, # If a record is not found in the relationship or the database, we error
on_match: :ignore, # If a record is found in the relationship we don't change it
on_missing: :ignore, # If a record is not found in the input, we ignore it
)
Provide opts
to customize/override the behavior.
Specs
apply_attributes(t()) :: {:ok, Ash.Resource.record()} | {:error, t()}
Returns the original data with attribute changes merged, if the changeset is valid.
Specs
before_action( t(), (t() -> t() | {t(), %{notificactions: [Ash.Notifier.Notification.t()]}}) ) :: t()
Adds a before_action hook to the changeset.
Adds a change to the changeset, unless the value matches the existing value
Specs
Calls change_attribute/3
for each key/value pair provided
Specs
Change an attribute only if is not currently being changed
Specs
Change an attribute if is not currently being changed, by calling the provided function
Use this if you want to only perform some expensive calculation for an attribute value only if there isn't already a change for that attribute
Specs
Returns true if an attribute exists in the changes
Specs
Returns true if a relationship exists in the changes
Clears an attribute or relationship change off of the changeset
Remove an argument from the changeset
Ensure the the specified attributes are nil
in the changeset results.
Specs
fetches the value of an argument provided to the changeset or :error
Specs
Gets the new value for an attribute, or :error
if it is not being changed
Constructs a changeset for a given create action, and validates it.
Anything that is modified prior to for_create/4
is validated against the rules of the action, while anything after it is not.
This runs any change
s contained on your action. To have your logic execute only during the action, you can use after_action/2
or before_action/2
.
Multitenancy is not validated until an action is called. This allows you to avoid specifying a tenant until just before calling the api action.
Params
params
may be attributes, relationships, or arguments. You can safely pass user/form input directly into this function.
Only public attributes and relationships are supported. If you want to change private attributes as well, see the
Customization section below. params
are stored directly as given in the params
field of the changeset, which is used
Opts
:relationships
- 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 eitherone of the preset manage types: [:replace, :append, :remove, :direct_control, :create]
explicit options, in the form of
{:manage, [...opts]}
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?
- If set totrue
, values are only required when the action is run (instead of immediately). The default value isfalse
.:actor
- set the actor, which can be used in anyAsh.Resource.Change
s configured on the action. (in thecontext
argument):tenant
- set the tenant on the changeset
Customization
A changeset can be provided as the first argument, instead of a resource, to allow setting specific attributes ahead of time.
For example:
MyResource
|> Changeset.change_attribute(:foo, 1)
|> Changeset.for_create(:create, ...opts)
Once a changeset has been validated by for_create/4
(or for_update/4
), it isn't validated again in the action.
New changes added are validated individually, though. This allows you to create a changeset according
to a given action, and then add custom changes if necessary.
Constructs a changeset for a given destroy action, and validates it.
Opts
:actor
- set the actor, which can be used in anyAsh.Resource.Change
s configured on the action. (in thecontext
argument):tenant
- set the tenant on the changeset
Anything that is modified prior to for_destroy/4
is validated against the rules of the action, while anything after it is not.
Once a changeset has been validated by for_destroy/4
, it isn't validated again in the action.
New changes added are validated individually, though. This allows you to create a changeset according
to a given action, and then add custom changes if necessary.
Constructs a changeset for a given update action, and validates it.
Anything that is modified prior to for_update/4
is validated against the rules of the action, while anything after it is not.
See for_create/4
for more information
Specs
Changes an attribute even if it isn't writable
Specs
Calls force_change_attribute/3
for each key/value pair provided
Specs
Force change an attribute if is not currently being changed, by calling the provided function
See change_new_attribute_lazy/3
for more.
Specs
Gets the value of an argument provided to the changeset
Specs
Gets the changing value or the original value of an attribute
Specs
Gets the original value for an attribute
Manages the related records by creating, updating, or destroying them as necessary.
Keep in mind that the default values for all on_*
are :ignore
, meaning nothing will happen
unless you provide instructions.
The input provided to manage_relationship
should be a map, in the case of to_one relationships, or a list of maps
in the case of to_many relationships. The following steps are followed for each input provided:
- The input is checked against the currently related records to find any matches. The primary key and unique identities are used to find matches.
- For any input that had a match in the current relationship, the
:on_match
behavior is triggered - For any input that does not have a match:
- if there is
on_lookup
behavior:- we try to find the record in the data layer.
- if the record is found, the on_lookup behavior is triggered
- if the record is not found, the
on_no_match
behavior is triggered
- if there is no
on_lookup
behavior:- the
on_no_match
behavior is triggered
- the
- if there is
- finally, for any records present in the current relationship that had no match in the input, the
on_missing
behavior is triggered
Options
:type
- If thetype
is specified, the default values of each option is modified to match thattype
of operation.
This allows for specifying certain operations much more succinctly. The defaults that are modified are listed below:replace
[ on_lookup: :relate, on_no_match: :error, on_match: :ignore, on_missing: :unrelate ]
:append
[ on_lookup: :relate, on_no_match: :error, on_match: :ignore, on_missing: :ignore ]
:remove
[ on_no_match: :error, on_match: :unrelate, on_missing: :ignore ]
:direct_control
[ on_lookup: :ignore, on_no_match: :create, on_match: :update, on_missing: :destroy ]
:create
[ on_no_match: :create, on_match: :ignore ]
:authorize?
- Authorize reads and changes to the destination records, if the primary change is being authorized as well. The default value istrue
.:on_no_match
- instructions for handling records where no matching record existed in the relationship:ignore
(default) - those inputs are ignored:create
- the records are created using the destination's primary create action{:create, :action_name}
- the records are created using the specified action on the destination resource{:create, :action_name, :join_table_action_name, [:list, :of, :join_table, :params]}
- Same as{:update, :action_name}
but takes the list of params specified out and applies them when creating the join table row.:error
- an eror is returned indicating that a record would have been created- If
on_lookup
is set, and the data contained a primary key or identity, then the error is aNotFound
error - Otherwise, an
InvalidRelationship
error is returned The default value is:ignore
.
- If
:on_lookup
- Before creating a record(because no match was found in the relationship), the record can be looked up and related.:ignore
(default) - Does not look for existing entries (matches in the current relationship are still considered updates):relate
- Same as calling{:relate, primary_action_name}
{:relate, :action_name}
- the records are looked up by primary key/the first identity that is found (using the primary read action), and related. The action should be:- many_to_many - a create action on the join resource
- has_many - an update action on the destination resource
- has_one - an update action on the destination resource
- belongs_to - an update action on the source resource
{:relate, :action_name, :read_action_name}
- Same as the above, but customizes the read action called to search for matches.:relate_and_update
- Same as:relate
, but the remaining parameters from the lookup are passed into the action that is used to change the relationship key{:relate_and_update, :action_name}
- Same as the above, but customizes the action used. The action should be:- many_to_many - a create action on the join resource
- has_many - an update action on the destination resource
- has_one - an update action on the destination resource
- belongs_to - an update action on the source resource
{:relate_and_update, :action_name, :read_action_name}
- Same as the above, but customizes the read action called to search for matches.{:relate_and_update, :action_name, :read_action_name, [:list, :of, :join_table, :params]}
- Same as the above, but uses the provided list of parameters when creating the join row (only relevant for many to many relationships). Use:all
to only update the join table row, and pass all parameters to its action The default value is:ignore
.
:on_match
- instructions for handling records where a matching record existed in the relationship already:ignore
(default) - those inputs are ignored:update
- the record is updated using the destination's primary update action{:update, :action_name}
- the record is updated using the specified action on the destination resource{:update, :action_name, :join_table_action_name, [:list, :of, :params]}
- Same as{:update, :action_name}
but takes the list of params specified out and applies them as an update to the join table row (only valid for many to many).:error
- an eror is returned indicating that a record would have been updated:no_match
- ignores the primary key match and follows the on_no_match instructions with these records instead.:unrelate
- the related item is not destroyed, but the data is "unrelated", making this behave likeremove_from_relationship/3
. The action should be:- many_to_many - the join resource row is destroyed
- has_many - the destination_field (on the related record) is set to
nil
- has_one - the destination_field (on the related record) is set to
nil
- belongs_to - the source_field (on this record) is set to
nil
{:unrelate, :action_name}
- the record is unrelated using the provided update action. The action should be:- many_to_many - a destroy action on the join resource
- has_many - an update action on the destination resource
- has_one - an update action on the destination resource
- belongs_to - an update action on the source resource The default value is
:ignore
.
:on_missing
- instructions for handling records that existed in the current relationship but not in the input:ignore
(default) - those inputs are ignored:destroy
- the record is destroyed using the destination's primary destroy action{:destroy, :action_name}
- the record is destroyed using the specified action on the destination resource{:destroy, :action_name, :join_resource_action_name, [:join, :keys]}
- the record is destroyed using the specified action on the destination resource, but first the join resource is destroyed with its specified action:error
- an error is returned indicating that a record would have been updated:unrelate
- the related item is not destroyed, but the data is "unrelated", making this behave likeremove_from_relationship/3
. The action should be:- many_to_many - the join resource row is destroyed
- has_many - the destination_field (on the related record) is set to
nil
- has_one - the destination_field (on the related record) is set to
nil
- belongs_to - the source_field (on this record) is set to
nil
{:unrelate, :action_name}
- the record is unrelated using the provided update action. The action should be:- many_to_many - a destroy action on the join resource
- has_many - an update action on the destination resource
- has_one - an update action on the destination resource
- belongs_to - an update action on the source resource The default value is
:ignore
.
:relationships
- A keyword list of instructions for nested relationships. The default value is[]
.:meta
- Freeform data that will be retained along with the options, which can be used to track/manage the changes that are added to therelationships
key.
Each call to this function adds new records that will be handled according to their options. For example, if you tracked "tags to add" and "tags to remove" in separate fields, you could input them like so:
changeset
|> Changeset.manage_relationship(
:tags,
[%{name: "backend"}],
on_lookup: :relate, #relate that tag if it exists in the database
on_no_match: :error # error if a tag with that name doesn't exist
)
|> Changeset.manage_relationship(
:tags,
[%{name: "frontend"}],
on_no_match: :error, # error if a tag with that name doesn't exist in the relationship
on_match: :unrelate # if a tag with that name is related, unrelate it
)
When calling this multiple times with the on_missing
option set, the list of records that are considered missing are checked
after each set of inputs is processed. For example, if you manage the relationship once with on_missing: :unrelate
, the records
missing from your input will be removed, and then your next call to manage_relationship
will be resolved (with those records unrelated).
For this reason, it is suggested that you don't call this function multiple times with an on_missing
instruction, as you may be
surprised by the result.
If you want the input to update existing entities, you need to ensure that the primary key (or unique identity) is provided as part of the input. See the example below:
changeset,
|> Changeset.manage_relationship(
:comments,
[%{rating: 10, contents: "foo"}],
on_no_match: {:create, :create_action},
on_missing: :ignore
)
|> Changeset.manage_relationship(
:comments,
[%{id: 10, rating: 10, contents: "foo"}],
on_match: {:update, :update_action},
on_no_match: {:create, :create_action})
This is a simple way to manage a relationship. If you need custom behavior, you can customize the action that is called, which allows you to add arguments/changes. However, at some point you may want to forego this function and make the changes yourself. For example:
input = [%{id: 10, rating: 10, contents: "foo"}]
changeset
|> Changeset.after_action(fn _changeset, result ->
# An example of updating comments based on a result of other changes
for comment <- input do
comment = MyApi.get(Comment, comment.id)
comment
|> Map.update(:rating, 0, &(&1 * result.rating_weight))
|> MyApi.update!()
end
{:ok, result}
end)
Using records as input
Records can be supplied as the input values. If you do:
- if it would be looked up due to
on_lookup
, the record is used as-is - if it would be created due to
on_no_match
, the record is used as-is - Instead of specifying
join_keys
, those keys must go in__metadata__.join_keys
. Ifjoin_keys
is specified in the options, it is ignored.
For example:
post1 =
changeset
|> Api.create!()
|> Ash.Resource.Info.put_metadata(:join_keys, %{type: "a"})
post1 =
changeset2
|> Api.create!()
|> Ash.Resource.Info.put_metadata(:join_keys, %{type: "b"})
author = Api.create!(author_changeset)
Ash.Changeset.manage_relationship(
author,
:posts,
[post1, post2],
on_lookup: :relate
)
Specs
manage_relationship_opts(manage_relationship_type()) :: Keyword.t()
Specs
new(Ash.Resource.t() | Ash.Resource.record(), params :: map()) :: t()
Return a changeset over a resource or a record. params
can be either attributes, relationship values or arguments.
If you are using external input, you almost certainly want to use Ash.Changeset.for_<action_type>
. However, you can
use Ash.Changeset.new/2
to start a changeset and make a few changes prior to calling for_action
. For example:
Ash.Changeset.new()
|> Ash.Changeset.change_attribute(:name, "foobar")
|> Ash.Changeset.for_action(...)
Anything that is modified prior to for_action
is validated against the rules of the action, while anything after it is not.
This changeset does not consider an action, and so allows you to change things with minimal validation. Values are
validated when changed, and the existence of attributes and relationships are validated. If you want to essentially
"run an action", and get back a changeset with any errors that would be generated by that action (with the exception
of errors that can only be generated by the data layer), use for_action/2
.
Additionally, this format only supports supplying attributes in the params. This is because we don't know what the
behavior should be for relationship changes, nor what arguments are available. You can manage them yourself with
the functions that allow managing arguments/relationships that are provided in this module, e.g set_argument/3
and
replace_relationship/3
Specs
Puts a key/value in the changeset context that can be used later
Do not use the private
key in your custom context, as that is reserved for internal use.
remove_from_relationship(changeset, relationship, record_or_records, opts \\ [])
View SourceSpecs
remove_from_relationship( t(), atom(), Ash.Resource.record() | map() | term() | [Ash.Resource.record() | map() | term()], Keyword.t() ) :: t()
Removes a record or a list of records to a relationship.
Alias for:
manage_relationship(changeset, relationship, record_or_records,
on_no_match: :error, # If a record is not found in the relationship, we error
on_match: :unrelate, # If a record is found in the relationship we unrelate it
on_missing: :ignore, # If a record is not found in the relationship
authorize?: false
)
replace_relationship(changeset, relationship, record_or_records, opts \\ [])
View SourceSpecs
replace_relationship( t(), atom(), Ash.Resource.record() | map() | term() | [Ash.Resource.record() | map() | term()] | nil, Keyword.t() ) :: t()
Alias for:
manage_relationship(
changeset,
relationship,
record_or_records,
on_lookup: :relate, # If a record is not found in the relationship, but is found in the database, relate it and apply the input as an update
on_no_match: :error, # If a record is not found in the relationship or the database, we error
on_match: :ignore, # If a record is found in the relationship we make no changes to it
on_missing: :unrelate, # If a record is not found in the relationship, we unrelate it
authorize?: false
)
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
fields unless the replace?
option is provided with a value of true
.
If a field has been deselected, selecting it again will override that (because a single list of fields is tracked for selection)
Primary key and private attributes are always selected and cannot be deselected.
When attempting to load a relationship (or manage it with Ash.Changeset.manage_relationship/3
),
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.
Add an argument to the changeset, which will be provided to the action
Merge a map of arguments to the arguments list
Specs
Deep merges the provided map into the changeset context that can be used later
Do not use the private
key in your custom context, as that is reserved for internal use.
Specs
Specs
with_hooks( t(), (t() -> {:ok, Ash.Resource.record(), %{notifications: [Ash.Notifier.Notification.t()]}} | {:error, term()}) ) :: {:ok, term(), t(), %{notifications: [Ash.Notifier.Notification.t()]}} | {:error, term()}
Wraps a function in the before/after action hooks of a changeset.
The function takes a changeset and if it returns
{:ok, result}
, the result will be passed through the after
action hooks.