ash v1.33.0 Ash.Changeset 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
.
Primary Keys
For relationship manipulation using append_to_relationship/3
, remove_from_relationship/3
and replace_relationship/3
there are three types that can be used for primary keys:
1.) An instance of the resource in question.
2.) If the primary key is just a single field, i.e :id
, then a single value, i.e 1
3.) A map of keys to values representing the primary key, i.e %{id: 1}
or %{id: 1, org_id: 2}
Join Attributes
For many to many relationships, the attributes on a join relationship may be set while relating items by passing a tuple of the primary key and the changes to be applied. This is done via upserts, so update validations on the join resource are not applied, but create validations are.
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"}}
])
Manage relationship vs append/replace/remove
Manage relationship
Ash.Changeset.manage_relationship/4
is for creating/updating/destroying related items. A simple example
is for creating a comment and adding it to a post.
post
|> Ash.Changeset.manage_relationship(
:comments,
[%{body: "this post is great!"}],
on_destroy: :ignore,
on_update: :error
)
|> MyApp.MyApi.update!(actor: current_user)
We configured it to ignore any "destroys", meaning "don't worry about the comments that are related but not in this list."
We also configured it to error on updates, meaning "this shouldn't change any existing comments"
We left on_create
as the default, which will call the primary create action on the destination.
User input should not be passed directly into this function. See manage_relationship/4
for more.
By default, these changes on the destination resources follow the authorization rules, if any, on that resource
Append/Replace/Remove
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.
These changes on the destination resources do not follow authorization rules of the destination resource.
For example, updating a has_many
relationship could involve changing the destination field to point at a different record.
User input should not be passed directly into this function. See manage_relationship/4
for more.
Instead add an append_to_relationship
, remove_from_relationship
or replace_relationship
to the action itself.
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. Stacks with previous removals/additions.
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
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.
Removes a record or a list of records to a relationship. Stacks with previous removals/additions.
Replaces the value of a relationship. Any previous additions/removals are cleared.
Add an argument to the changeset, which will be provided to the action
Merge a map of arguments to the arguments list
Wraps a function in the before/after action hooks of a changeset.
Link to this section Types
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(), requests: term(), resource: 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.record() -> {:ok, Ash.record()} | {:ok, Ash.record(), [Ash.notification()]} | {:error, term()}) ) :: t()
Adds an after_action hook to the changeset.
append_to_relationship(changeset, relationship, record_or_records)
View SourceSpecs
append_to_relationship(t(), atom(), Ash.primary_key() | [Ash.primary_key()]) :: t()
Appends a record or a list of records to a relationship. Stacks with previous removals/additions.
Accepts a primary key or a list of primary keys. See the section on "Primary Keys" in the module documentation for more.
For many to many relationships, accepts changes for any join_attributes
configured on
the resource. See the section on "Join Attributes" in the module documentation for more.
Does not authorize changes on the destination resource, nor notify those changes.
Cannot be used with belongs_to
or has_one
relationships.
See replace_relationship/3
for manipulating belongs_to
and has_one
relationships.
Specs
apply_attributes(t()) :: {:ok, Ash.record()} | {:error, t()}
Returns the original data with attribute changes merged, if the changeset is valid.
Specs
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
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.
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. See the Relationships section below.:actor
- set the actor, which can be used in anyAsh.Resource.Change
s configured on the action. (in thecontext
argument)
Relationships
By default, any relationships are replaced via replace_relationship
. To change this behavior, provide the
relationships
option.
For example:
Ash.Changeset.for_create(MyResource, :create, params, relationships: [relationship: :append, other_relationship: :remove])
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.
Pass an actor
option to specify the actor
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.
Generally speaking, this function should not be used with user input. If you want to accept user
input to manage a relationship, e.g via a form, api, or controller input, instead add a managed_relationship
to your action. See the DSL documentation for more on that
Unlike append_to_relationship/4
, replace_relationship/3
and remove_from_relationship/3
,
this will actually make changes to the non-relationship fields of the destination resource.
For the other functions, the only authorization is involved is the authorization on this resource,
however manage_relationship/4
will authorization/validate each individual operation.
If you want the input to update existing entities (when the type
is :replace
, the default),
you need to ensure that the primary key is provided as part of the input. See the example below:
Changeset.manage_relationship(
changeset,
:comments,
[%{rating: 10, contents: "foo"}],
on_create: {:create, :create_action},
on_destroy: :ignore
)
Changeset.manage_relationship(
changeset,
:comments,
[%{id: 10, rating: 10, contents: "foo"}],
on_update: {:update, :update_action},
on_create: {:create, :create_action})
Options
:authorize?
- Authorize changes to the destination records, if the primary change is being authorized as well. The default value istrue
.:on_create
- instructions for handling records where no matching record existed in the relationship:create
(default) - 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.:ignore
- those inputs are ignored:error
- an eror is returned indicating that a record would have been created The default value is:create
.
:on_update
- instructions for handling records where a matching record existed in the relationship already:update
(default) - 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).:ignore
- those inputs are ignored:error
- an eror is returned indicating that a record would have been updated:create
- ignores the primary key match and follows the create instructions with these records instead.:destroy
- follows the destroy instructions for any records with matching primary keys The default value is:update
.
:on_destroy
- instructions for handling records that existed in the current relationship but not in the input:destroy
(default) - 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}
- the record is destroyed using the specified action on the destination resource, but first the join resource is destroyed with its specified action:ignore
- those inputs are ignored: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
- 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.- 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
:destroy
.
Mixing with other relationship functions
If mixed with append_to_relationship/3
, remove_from_relationship/3
and replace_relationship/3
, those actions will
happen first. After all of those changes have been made, the relationship will be "managed" according to the options provided
to this.
%Post{}
|> Ash.Changeset.new()
|> Ash.Changeset.manage_relationship(:comments, [%{text: "bar"}])
|> Ash.Changeset.append_to_relationship(:comments, [%{text: "foo"}])
|> Api.update!()
# %Post{comments: [%Comment{text: "bar"}, %Comment{text: "foo"}]}
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)
Specs
new(Ash.resource() | Ash.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
remove_from_relationship(changeset, relationship, record_or_records)
View SourceSpecs
remove_from_relationship(t(), atom(), Ash.primary_key() | [Ash.primary_key()]) :: t()
Removes a record or a list of records to a relationship. Stacks with previous removals/additions.
Accepts a primary key or a list of primary keys. See the section on "Primary Keys" in the module documentation for more.
Does not authorize changes on the destination resource, nor notify those changes.
Cannot be used with belongs_to
or has_one
relationships.
See replace_relationship/3
for manipulating those relationships.
Specs
replace_relationship(t(), atom(), Ash.primary_key() | [Ash.primary_key()] | nil) :: t()
Replaces the value of a relationship. Any previous additions/removals are cleared.
Accepts a primary key or a list of primary keys. See the section on "Primary Keys" in the module documentation for more.
For many to many relationships, accepts changes for any join_attributes
configured on
the resource. See the section on "Join Attributes" in the module documentation for more.
For a has_many
or many_to_many
relationship, this means removing any currently related
records that are not present in the replacement list, and creating any that do not exist
in the data layer.
For a belongs_to
or has_one
, replace with a nil
value to unset a relationship.
Does not authorize changes on the destination resource, nor notify those changes.
Add an argument to the changeset, which will be provided to the action
Merge a map of arguments to the arguments list
Specs
Specs
Specs
with_hooks( t(), (t() -> {:ok, Ash.record(), %{notifications: [Ash.notification()]}} | {:error, term()}) ) :: {:ok, term(), t(), %{notifications: [Ash.notification()]}} | {: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.