add_tag but unique

A product has a many_to_many relationship to a tag (via a product_tag resource).
lib/app/shop/resources/product.ex
[...]
create :create do
primary? true
argument :tags, {:array, :map}

argument :add_tag, :map do
allow_nil? false
end

change manage_relationship(:tags, type: :append_and_remove, on_no_match: :create)
change manage_relationship(:add_tag, :tags, type: :create)
end
[...]
[...]
create :create do
primary? true
argument :tags, {:array, :map}

argument :add_tag, :map do
allow_nil? false
end

change manage_relationship(:tags, type: :append_and_remove, on_no_match: :create)
change manage_relationship(:add_tag, :tags, type: :create)
end
[...]
This works nicely:
iex(6)> banana = App.Shop.Product.create!(%{name: "Banana", add_tag: %{name: "Yellow"}})
iex(6)> banana = App.Shop.Product.create!(%{name: "Banana", add_tag: %{name: "Yellow"}})
Unfortunately I can also add another "Yellow" tag via an update:
iex(7)> App.Shop.Product.update!(banana, %{add_tag: %{name: "Yellow"}}).tags |> Enum.map(& &1.name)
["Yellow", "Yellow"]
iex(7)> App.Shop.Product.update!(banana, %{add_tag: %{name: "Yellow"}}).tags |> Enum.map(& &1.name)
["Yellow", "Yellow"]
Obviously this should not happen. The "Yellow" tag should be unique. The longer I think about this the lesser I know how to solve it. Should I through a validation error? How can I do this? Should I just OK it but not adding another tag? How would I do that?
10 Replies
barnabasj
barnabasj2y ago
You can add identities to your tag resource making the names unique, in that case the manage_relationsgip can use the name to lookup existing tags. You might need to add some options to the manage_relationship call, telling it which identities to use.
Stefan Wintermeyer
Can you give me an example of how to setup an identity? https://hexdocs.pm/ash/identities.html doesn't have any examples and the https://ash-hq.org internal search engine doesn't work right now. Here's my tag resource:
defmodule App.Shop.Tag do
use Ash.Resource, data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id
attribute :name, :string
end

relationships do
many_to_many :products, App.Shop.Product do
through App.Shop.ProductTag
source_attribute_on_join_resource :tag_id
destination_attribute_on_join_resource :product_id
end
end

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

create :create do
primary? true
argument :products, {:array, :map}
change manage_relationship(:products, type: :append_and_remove, on_no_match: :create)
end
end

code_interface do
define_for App.Shop
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :by_name, get_by: [:name], action: :read
define :update
define :destroy
end
end
defmodule App.Shop.Tag do
use Ash.Resource, data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id
attribute :name, :string
end

relationships do
many_to_many :products, App.Shop.Product do
through App.Shop.ProductTag
source_attribute_on_join_resource :tag_id
destination_attribute_on_join_resource :product_id
end
end

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

create :create do
primary? true
argument :products, {:array, :map}
change manage_relationship(:products, type: :append_and_remove, on_no_match: :create)
end
end

code_interface do
define_for App.Shop
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :by_name, get_by: [:name], action: :read
define :update
define :destroy
end
end
Ash HQ
Ash Framework
A declarative foundation for ambitious Elixir applications. Model your domain, derive the rest.
barnabasj
barnabasj2y ago
https://hexdocs.pm/ash/dsl-ash-resource.html#identities unfortunately I only have my phone right now, so it's a bit hard to write code
ZachDaniel
ZachDaniel2y ago
yep, from that guide:
identities do
identity :unique_name, [:name]
end
identities do
identity :unique_name, [:name]
end
and then:
change manage_relationship(:products, type: :append_and_remove, on_no_match: :create, use_identities: [:unique_name])
change manage_relationship(:products, type: :append_and_remove, on_no_match: :create, use_identities: [:unique_name])
Stefan Wintermeyer
Do I use it wrong? Here's the tag resource:
defmodule App.Shop.Tag do
use Ash.Resource, data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id
attribute :name, :string
end

identities do
identity :unique_name, [:name]
end

relationships do
many_to_many :products, App.Shop.Product do
through App.Shop.ProductTag
source_attribute_on_join_resource :tag_id
destination_attribute_on_join_resource :product_id
end
end

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

create :create do
primary? true
argument :products, {:array, :map}

change manage_relationship(:products,
type: :append_and_remove,
on_no_match: :create,
use_identities: [:unique_name]
)
end
end

code_interface do
define_for App.Shop
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :by_name, get_by: [:name], action: :read
define :update
define :destroy
end
end
defmodule App.Shop.Tag do
use Ash.Resource, data_layer: Ash.DataLayer.Ets

attributes do
uuid_primary_key :id
attribute :name, :string
end

identities do
identity :unique_name, [:name]
end

relationships do
many_to_many :products, App.Shop.Product do
through App.Shop.ProductTag
source_attribute_on_join_resource :tag_id
destination_attribute_on_join_resource :product_id
end
end

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

create :create do
primary? true
argument :products, {:array, :map}

change manage_relationship(:products,
type: :append_and_remove,
on_no_match: :create,
use_identities: [:unique_name]
)
end
end

code_interface do
define_for App.Shop
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :by_name, get_by: [:name], action: :read
define :update
define :destroy
end
end
With that I get this error:
** (EXIT from #PID<0.98.0>) an exception was raised:
** (Spark.Error.DslError) [App.Shop.Tag]
The data layer does not support native checking of identities.

Identities: unique_name

Must specify the `pre_check_with` option.

(spark 1.1.39) lib/spark/dsl/extension.ex:560: Spark.Dsl.Extension.raise_transformer_error/2
(elixir 1.15.5) lib/enum.ex:4830: Enumerable.List.reduce/3
(elixir 1.15.5) lib/enum.ex:2564: Enum.reduce_while/3
(elixir 1.15.5) lib/enum.ex:984: Enum."-each/2-lists^foreach/1-0-"/2
(elixir 1.15.5) lib/module/parallel_checker.ex:271: Module.ParallelChecker.check_module/3
(elixir 1.15.5) lib/module/parallel_checker.ex:82: anonymous fn/6 in Module.ParallelChecker.spawn/4
** (EXIT from #PID<0.98.0>) an exception was raised:
** (Spark.Error.DslError) [App.Shop.Tag]
The data layer does not support native checking of identities.

Identities: unique_name

Must specify the `pre_check_with` option.

(spark 1.1.39) lib/spark/dsl/extension.ex:560: Spark.Dsl.Extension.raise_transformer_error/2
(elixir 1.15.5) lib/enum.ex:4830: Enumerable.List.reduce/3
(elixir 1.15.5) lib/enum.ex:2564: Enum.reduce_while/3
(elixir 1.15.5) lib/enum.ex:984: Enum."-each/2-lists^foreach/1-0-"/2
(elixir 1.15.5) lib/module/parallel_checker.ex:271: Module.ParallelChecker.check_module/3
(elixir 1.15.5) lib/module/parallel_checker.ex:82: anonymous fn/6 in Module.ParallelChecker.spawn/4
barnabasj
barnabasj2y ago
I have used identities only with the postgres layer before, looks like you need to configure the pre_check_with option and pass it the API that holds the resource
ZachDaniel
ZachDaniel2y ago
Yes, Ash.DataLayer.Ets doesn't have unique enforcement in the data layer, so it has to do a check before inserting Ideally we would build that feature into the ETS data layer, but until then it requires that setting to be configured
Stefan Wintermeyer
How does that work code wise? https://ash-hq.org/docs/dsl/ash-resource#identities-identity-pre_check_with doesn't include an example.
ZachDaniel
ZachDaniel2y ago
pre_check_with Your.Api in the identity definition
identity :name, [:field] do
pre_check_with API
end
identity :name, [:field] do
pre_check_with API
end
Stefan Wintermeyer
For the archive:
identities do
# identity :unique_name, [:name] <1>

identity :name, [:name] do
pre_check_with App.Shop
end
end
identities do
# identity :unique_name, [:name] <1>

identity :name, [:name] do
pre_check_with App.Shop
end
end
<1> Use with a PostgreSQL DB.

Did you find this page helpful?