Attributes on `many_to_many` join/through resources.

Considering the following resources.
defmodule Formula do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id

create_timestamp :created_at
update_timestamp :updated_at
end

actions do
defaults([:create, :read, :update, :destroy])

create :define do

argument :formula, {:array, :map}

change manage_relationship(:formula, :reactants,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end

end

relationships do
many_to_many :reactants, Reactant do
through FormulaReactant
source_attribute_on_join_resource :formula_id
destination_attribute_on_join_resource :reactant_id
end
end

end
defmodule FormulaReactant do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets
attributes do
attribute :quantity, :decimal do
allow_nil? false
constraints [min: 0.0]
end

attribute :unit, :string do
allow_nil? false
constraints trim?: true, allow_empty?: false, min_length: 1
end
end

actions do
defaults [:create, :read, :update, :destroy]

end

relationships do
belongs_to :formula, Formula, primary_key?: true, allow_nil?: false
belongs_to :reactant, Reactant, primary_key?: true, allow_nil?: false
end

end
defmodule Reactant do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id

attribute :name, :string

create_timestamp :created_at
update_timestamp :updated_at
end

actions do
defaults([:create, :read, :update, :destroy])
end
end
defmodule Formula do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id

create_timestamp :created_at
update_timestamp :updated_at
end

actions do
defaults([:create, :read, :update, :destroy])

create :define do

argument :formula, {:array, :map}

change manage_relationship(:formula, :reactants,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end

end

relationships do
many_to_many :reactants, Reactant do
through FormulaReactant
source_attribute_on_join_resource :formula_id
destination_attribute_on_join_resource :reactant_id
end
end

end
defmodule FormulaReactant do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets
attributes do
attribute :quantity, :decimal do
allow_nil? false
constraints [min: 0.0]
end

attribute :unit, :string do
allow_nil? false
constraints trim?: true, allow_empty?: false, min_length: 1
end
end

actions do
defaults [:create, :read, :update, :destroy]

end

relationships do
belongs_to :formula, Formula, primary_key?: true, allow_nil?: false
belongs_to :reactant, Reactant, primary_key?: true, allow_nil?: false
end

end
defmodule Reactant do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id

attribute :name, :string

create_timestamp :created_at
update_timestamp :updated_at
end

actions do
defaults([:create, :read, :update, :destroy])
end
end
The following create attempt fails.
Formula
|>Ash.Changeset.for_create(:define, %{formula: [%{name: "pinata"}]})
|>Api.create()
Formula
|>Ash.Changeset.for_create(:define, %{formula: [%{name: "pinata"}]})
|>Api.create()
This is because attributes :quantity and :unit are not being set on FormulaReactant. How would a create action on Formula be setup to set both attributes?
3 Replies
barnabasj
barnabasj2y ago
I don't think that there is anything out of the box that helps with this case, but the many_to_many also adds another relationship to your resource for the through resource https://ash-hq.org/docs/dsl/ash-resource#relationships-many_to_many-join_relationship So I think what you could do is something like this.
defmodule Formula do

...

actions do
defaults([:create, :read, :update, :destroy])

create :define do

argument :formula, {:array, :map}

change manage_relationship(:formula, :reactants_join_assoc,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end
end
end

defmodule FormulaReactant do

...

actions do
defaults [:read, :destroy]

create :create do
argument :formula, :map

pimary? true

change manage_relationship(:reactant, :reactant,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end

create :update do
argument :formula, :map

pimary? true

change manage_relationship(:reactant, :reactant,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end
end
end
defmodule Formula do

...

actions do
defaults([:create, :read, :update, :destroy])

create :define do

argument :formula, {:array, :map}

change manage_relationship(:formula, :reactants_join_assoc,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end
end
end

defmodule FormulaReactant do

...

actions do
defaults [:read, :destroy]

create :create do
argument :formula, :map

pimary? true

change manage_relationship(:reactant, :reactant,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end

create :update do
argument :formula, :map

pimary? true

change manage_relationship(:reactant, :reactant,
on_lookup: :relate,
on_no_match: :create,
on_match: :update,
on_missing: :unrelate
)
end
end
end
Formula
|>Ash.Changeset.for_create(:define, %{formula: [%{quanity: 1, unit: "kg", reactant: %{name: "pinata"}}]})
|>Api.create()
Formula
|>Ash.Changeset.for_create(:define, %{formula: [%{quanity: 1, unit: "kg", reactant: %{name: "pinata"}}]})
|>Api.create()
In a way passing the data through the related resources yourself
morfertaw
morfertawOP2y ago
Yeah, this is exactly what I was hoping for. Cheers!
barnabasj
barnabasj2y ago
Btw, you don't need to make the actions the primary actions, you can also specify a specific action per operation e.g on_no_match: {:create, :create_action_name}

Did you find this page helpful?