diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4cb6f060..88edd0190 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,28 +2,43 @@
-## [v3.0.9](https://github.com/ash-project/ash/compare/v3.0.8...v3.0.9) (2024-05-31)
+## [v3.0.10](https://github.com/ash-project/ash/compare/v3.0.9...v3.0.10) (2024-06-06)
+
+### Bug Fixes:
+
+- [Ash.Union] ensure that union types w/ explicit tags have constraints applied
+
+- [multitenancy] don't update tenant on update, instead enforce it
+- [compare/2 validation] Do not compare nil values in `compare` validation (#1223)
+- [bulk actions] ensure context is properly set on bulk manual action invocations
+### Improvements:
+
+- [Ash.Resource] detect invalid resources placed in relationships on domains verifier
+
+- [Ash.Resource] warn at compile time on types that don't define `atomic_update/2`
+
+## [v3.0.9](https://github.com/ash-project/ash/compare/v3.0.8...v3.0.9) (2024-05-31)
### Bug Fixes:
-* use correct boolean operation names in Filter.find/4 (#1214)
+- [Ash.Filter] use correct boolean operation names in Filter.find/4 (#1214)
-* when hydrating nested aggregates, use correct related resource/path pair
+- [aggregates] when hydrating nested aggregates, use correct related resource/path pair
-* check if in transaction before trying to roll it back
+- [aggregates] retain `ref_path` when authorizing aggregates
-* retain `ref_path` when authorizing aggregates
+- [relationship loading] ensure that belongs_to relationships are properly not reloaded with `lazy?: true`
-* ensure that belongs_to relationships are properly not reloaded with `lazy?: true`
+- [bulk actions] implement rollback on after hooks for bulk actions
-* implement rollback on after hooks for bulk actions
+- [bulk actions] check if in transaction before trying to roll it back
### Improvements:
-* compatibility with elixir 1.17
+- compatibility with elixir 1.17
## [v3.0.8](https://github.com/ash-project/ash/compare/v3.0.7...v3.0.8) (2024-05-28)
diff --git a/documentation/topics/actions/actions.md b/documentation/topics/actions/actions.md
index d5fda9fc9..adbfbbeaa 100644
--- a/documentation/topics/actions/actions.md
+++ b/documentation/topics/actions/actions.md
@@ -49,7 +49,7 @@ You could then pass in `%{name: "a name", description: "a description"}` to this
### Using `default_accept` for all actions
-The resource can have a `default_accept`, declared in its `actions` block, which will be used as the accept list for `create` and `destroy` actions, if they don't define one.
+The resource can have a `default_accept`, declared in its `actions` block, which will be used as the accept list for `create` and `update` actions, if they don't define one.
```elixir
actions do
diff --git a/lib/ash/actions/destroy/bulk.ex b/lib/ash/actions/destroy/bulk.ex
index 1dd4217f1..ee069ba2f 100644
--- a/lib/ash/actions/destroy/bulk.ex
+++ b/lib/ash/actions/destroy/bulk.ex
@@ -1686,9 +1686,9 @@ defmodule Ash.Actions.Destroy.Bulk do
|> Enum.flat_map(fn {_atomics, batch} ->
result =
case action.manual do
- {mod, opts} ->
+ {mod, manual_opts} ->
if function_exported?(mod, :bulk_destroy, 3) do
- mod.bulk_destroy(batch, opts, %{
+ mod.bulk_destroy(batch, manual_opts, %{
actor: opts[:actor],
select: opts[:select],
batch_size: opts[:batch_size],
diff --git a/lib/ash/actions/destroy/destroy.ex b/lib/ash/actions/destroy/destroy.ex
index 707cfbd98..fdf5dbed7 100644
--- a/lib/ash/actions/destroy/destroy.ex
+++ b/lib/ash/actions/destroy/destroy.ex
@@ -171,6 +171,8 @@ defmodule Ash.Actions.Destroy do
{:ok, new_data, _} ->
changeset = %{changeset | data: new_data}
+ changeset = set_tenant(changeset)
+
if changeset.action.manual do
{mod, action_opts} = changeset.action.manual
@@ -264,6 +266,20 @@ defmodule Ash.Actions.Destroy do
end
end
+ defp set_tenant(changeset) do
+ if changeset.tenant &&
+ Ash.Resource.Info.multitenancy_strategy(changeset.resource) == :attribute do
+ attribute = Ash.Resource.Info.multitenancy_attribute(changeset.resource)
+
+ {m, f, a} = Ash.Resource.Info.multitenancy_parse_attribute(changeset.resource)
+ attribute_value = apply(m, f, [changeset.to_tenant | a])
+
+ Ash.Changeset.filter(changeset, [{attribute, attribute_value}])
+ else
+ changeset
+ end
+ end
+
defp validate_manual_action_return_result!({:ok, %resource{}} = result, resource, _) do
result
end
diff --git a/lib/ash/actions/helpers.ex b/lib/ash/actions/helpers.ex
index 2c8a4735d..1399b1580 100644
--- a/lib/ash/actions/helpers.ex
+++ b/lib/ash/actions/helpers.ex
@@ -50,7 +50,7 @@ defmodule Ash.Actions.Helpers do
"""
end
- def validate_calculation_load!(other, _), do: other
+ def validate_calculation_load!(other, _), do: List.wrap(other)
defp set_context(%Ash.Changeset{} = changeset, context),
do: Ash.Changeset.set_context(changeset, context)
diff --git a/lib/ash/actions/update/bulk.ex b/lib/ash/actions/update/bulk.ex
index 9849543f6..02739d41b 100644
--- a/lib/ash/actions/update/bulk.ex
+++ b/lib/ash/actions/update/bulk.ex
@@ -1966,11 +1966,11 @@ defmodule Ash.Actions.Update.Bulk do
|> Enum.flat_map(fn {_atomics, batch} ->
result =
case action.manual do
- {mod, opts} ->
+ {mod, manual_opts} ->
if function_exported?(mod, context_key, 3) do
apply(mod, context_key, [
batch,
- opts,
+ manual_opts,
struct(context_struct,
actor: opts[:actor],
select: opts[:select],
diff --git a/lib/ash/actions/update/update.ex b/lib/ash/actions/update/update.ex
index fe412c4ac..8b676faff 100644
--- a/lib/ash/actions/update/update.ex
+++ b/lib/ash/actions/update/update.ex
@@ -5,6 +5,7 @@ defmodule Ash.Actions.Update do
require Ash.Tracer
require Logger
+ import Ash.Expr
@spec run(Ash.Domain.t(), Ash.Resource.record(), Ash.Resource.Actions.action(), Keyword.t()) ::
{:ok, Ash.Resource.record(), list(Ash.Notifier.Notification.t())}
@@ -631,7 +632,7 @@ defmodule Ash.Actions.Update do
{m, f, a} = Ash.Resource.Info.multitenancy_parse_attribute(changeset.resource)
attribute_value = apply(m, f, [changeset.to_tenant | a])
- Ash.Changeset.force_change_attribute(changeset, attribute, attribute_value)
+ Ash.Changeset.filter(changeset, expr(^ref(attribute) == ^attribute_value))
else
changeset
end
diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex
index 68e7bd56e..e3e78acfa 100644
--- a/lib/ash/changeset/changeset.ex
+++ b/lib/ash/changeset/changeset.ex
@@ -151,7 +151,7 @@ defmodule Ash.Changeset do
%Ash.Filter{expression: nil} ->
empty()
- true ->
+ _ ->
concat("filter: ", to_doc(changeset.filter, opts))
end
diff --git a/lib/ash/domain/verifiers/validate_related_resource_inclusion.ex b/lib/ash/domain/verifiers/validate_related_resource_inclusion.ex
index 764988170..a7ba19b05 100644
--- a/lib/ash/domain/verifiers/validate_related_resource_inclusion.ex
+++ b/lib/ash/domain/verifiers/validate_related_resource_inclusion.ex
@@ -14,6 +14,12 @@ defmodule Ash.Domain.Verifiers.ValidateRelatedResourceInclusion do
for resource <- resources do
for relationship <- Ash.Resource.Info.relationships(resource) do
+ if !Ash.Resource.Info.resource?(relationship.destination) do
+ raise """
+ Relationship #{inspect(resource)}.#{relationship.name} has an invalid destination: #{inspect(relationship.destination)}.
+ """
+ end
+
domain = domain(relationship, dsl)
if !domain do
diff --git a/lib/ash/resource/validation/compare.ex b/lib/ash/resource/validation/compare.ex
index 5c5f5ec9d..5975deea4 100644
--- a/lib/ash/resource/validation/compare.ex
+++ b/lib/ash/resource/validation/compare.ex
@@ -55,7 +55,7 @@ defmodule Ash.Resource.Validation.Compare do
end
case value do
- {:ok, value} ->
+ {:ok, value} when not is_nil(value) ->
opts
|> Keyword.take([
:greater_than,
diff --git a/lib/ash/resource/verifiers/verify_actions_atomic.ex b/lib/ash/resource/verifiers/verify_actions_atomic.ex
index 10c70b9df..4c980e241 100644
--- a/lib/ash/resource/verifiers/verify_actions_atomic.ex
+++ b/lib/ash/resource/verifiers/verify_actions_atomic.ex
@@ -147,6 +147,13 @@ defmodule Ash.Resource.Verifiers.VerifyActionsAtomic do
end
defp not_atomic?(Ash.Type.Union, _), do: true
+
+ defp not_atomic?(type, %{type: :update}) when is_atom(type) do
+ type = Ash.Type.get_type(type)
+ Code.ensure_compiled!(type)
+ not function_exported?(type, :cast_atomic, 2)
+ end
+
defp not_atomic?(_, _), do: false
defp non_atomic_message(module, action_name, reason) do
diff --git a/lib/ash/type/union.ex b/lib/ash/type/union.ex
index b5c18c9b6..1bd5ede2e 100644
--- a/lib/ash/type/union.ex
+++ b/lib/ash/type/union.ex
@@ -383,7 +383,13 @@ defmodule Ash.Type.Union do
inner_constraints
) do
{:ok, value} ->
- {:ok, %Ash.Union{value: value, type: type_name}}
+ case Ash.Type.apply_constraints(type, value, inner_constraints) do
+ {:ok, value} ->
+ {:ok, %Ash.Union{value: value, type: type_name}}
+
+ {:error, other} ->
+ {:error, other}
+ end
error ->
error
diff --git a/mix.exs b/mix.exs
index f1ed71f49..5a5da359b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -6,7 +6,7 @@ defmodule Ash.MixProject do
A declarative, extensible framework for building Elixir applications.
"""
- @version "3.0.9"
+ @version "3.0.10"
def project do
[
@@ -116,7 +116,6 @@ defmodule Ash.MixProject do
~r"documentation/dsls"
]
],
- assets: %{"livebook.css" => "documentation/assets/livebook.css"},
skip_undefined_reference_warnings_on: [
"CHANGELOG.md",
"documentation/topics/reference/glossary.md",
@@ -138,7 +137,11 @@ defmodule Ash.MixProject do
before_closing_head_tag: fn type ->
if type == :html do
"""
-
+