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 """ - +