Is it possible to pass a map to args instead of a list of attributes in code_interface?

I'd like to pull off something like:
actions do
update :update_stats do
argument :stats, :map

Enum.map(arg(:stats), fn {key, value} ->
change set_attribute(key, value)
end)
end
end
actions do
update :update_stats do
argument :stats, :map

Enum.map(arg(:stats), fn {key, value} ->
change set_attribute(key, value)
end)
end
end
code_interface do

define :update_stats, args: [:stats]

end
code_interface do

define :update_stats, args: [:stats]

end
but I get the following error:
Compilation error in file lib/leaderboard/data/resources/wallet.ex ==
** (Protocol.UndefinedError) protocol Enumerable not implemented for {:_arg, :stats} of type Tuple
(elixir 1.14.4) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.14.4) lib/enum.ex:166: Enumerable.reduce/3
(elixir 1.14.4) lib/enum.ex:4307: Enum.map/2
lib/leaderboard/data/resources/wallet.ex:21: (module)
Compilation error in file lib/leaderboard/data/resources/wallet.ex ==
** (Protocol.UndefinedError) protocol Enumerable not implemented for {:_arg, :stats} of type Tuple
(elixir 1.14.4) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.14.4) lib/enum.ex:166: Enumerable.reduce/3
(elixir 1.14.4) lib/enum.ex:4307: Enum.map/2
lib/leaderboard/data/resources/wallet.ex:21: (module)
Is there a way to dynamically pass in args that you want to update on the resource? Or do you have to hard code the set_attributes? I'm trying to bypass the need to pass in each attribute in order every time. As their are many attributes and not all of them will be updated each time.
17 Replies
obsidian
obsidianOP2y ago
this is what the resource attributes look like:
attributes do
uuid_primary_key :id

attribute :address, :string

attribute :chain, :atom

attribute :bought_usd_volume, :float

attribute :bought_volume, :float

attribute :holdings_count, :integer

attribute :holdings_listed_count, :integer

attribute :realized_profit_loss, :float

attribute :realized_usd_profit_loss, :float

attribute :sold_usd_volume, :float

attribute :sold_volume, :float

attribute :unrealized_profit_loss, :float

attribute :unrealized_usd_profit_loss, :float

attribute :usd_value, :float

attribute :value, :float

attribute :timestamp, :utc_datetime

attribute :block_height, :integer
end
attributes do
uuid_primary_key :id

attribute :address, :string

attribute :chain, :atom

attribute :bought_usd_volume, :float

attribute :bought_volume, :float

attribute :holdings_count, :integer

attribute :holdings_listed_count, :integer

attribute :realized_profit_loss, :float

attribute :realized_usd_profit_loss, :float

attribute :sold_usd_volume, :float

attribute :sold_volume, :float

attribute :unrealized_profit_loss, :float

attribute :unrealized_usd_profit_loss, :float

attribute :usd_value, :float

attribute :value, :float

attribute :timestamp, :utc_datetime

attribute :block_height, :integer
end
ZachDaniel
ZachDaniel2y ago
I think you’re missing a key detail of how ash actions work 🙂 Each action has an accept list, which is a set of attributes it accepts as input and will write to the resource. By default, this is all public, writable attributes. And there is an optional map argument on all code interfaces that will pass those values through. So if you took out the args on your code interface, remove the argument/change from the action, you can then say update_stats(%{attr1: …, attr2: …}) And you can also say args: [:attribute] to make one attribute a positional argument, and then pass the map of optional stuff after that
obsidian
obsidianOP2y ago
ha it was that easy... For some reason that didn't come across to me in the action docs https://ash-hq.org/docs/guides/ash/latest/topics/actions Do you know where I can do a deep dive?
Ash HQ
Guide: Actions
Read the "Actions" guide on Ash HQ
obsidian
obsidianOP2y ago
Hmm... it seems that if I leave an attribute out, all the other attributes are being set to nil. Is that expected?
ZachDaniel
ZachDaniel2y ago
What do you mean by leave an attribute out? TBH if this isn’t made clear in the actions guide then we should improve that for sure.
obsidian
obsidianOP2y ago
False alarm. I passed in to update :value, and the returned wallet had every other attribute set to nil. But in the db, the data was correct.
ZachDaniel
ZachDaniel2y ago
interesting...were you selecting any data?
obsidian
obsidianOP2y ago
Yea you can see the logs here:
iex(45)> new_stats
%{usd_value: 2000.0}
iex(46)> Leaderboard.Data.Wallet.update_stats(wallet, new_stats)
commit []
{:ok,
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: nil,
bought_volume: nil,
holdings_count: nil,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>}
iex(47)> new_stats
%{usd_value: 2000.0}

iex(48)> stats2 = %{sold_volume: 1234.5}
%{sold_volume: 1234.5}
iex(49)> {:ok, [wallet]} = Leaderboard.Data.Wallet.find_wallet("SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks)

["SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks]
{:ok,
[
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: 1.0,
bought_volume: 2.0,
holdings_count: 3,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>
]}
iex(45)> new_stats
%{usd_value: 2000.0}
iex(46)> Leaderboard.Data.Wallet.update_stats(wallet, new_stats)
commit []
{:ok,
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: nil,
bought_volume: nil,
holdings_count: nil,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>}
iex(47)> new_stats
%{usd_value: 2000.0}

iex(48)> stats2 = %{sold_volume: 1234.5}
%{sold_volume: 1234.5}
iex(49)> {:ok, [wallet]} = Leaderboard.Data.Wallet.find_wallet("SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks)

["SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks]
{:ok,
[
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: 1.0,
bought_volume: 2.0,
holdings_count: 3,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>
]}
ZachDaniel
ZachDaniel2y ago
what is wallet that you're originally passing in?
obsidian
obsidianOP2y ago
Hmm I cleared my console, before this. But that's probably it. I'm passing in the original wallet that I queried from the db. So basically I queried
Did this action:
Did this action:
elixir iex(39)> Leaderboard.Data.Wallet.update_stats(wallet, stats) commit [] {:ok, #Leaderboard.Data.Wallet< meta: #Ecto.Schema.Metadata<:loaded, "wallets">, id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc", address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", chain: :stacks, bought_usd_volume: 1.0, bought_volume: 2.0, holdings_count: 3, holdings_listed_count: nil, realized_profit_loss: nil, realized_usd_profit_loss: nil, sold_usd_volume: nil, sold_volume: nil, unrealized_profit_loss: nil, unrealized_usd_profit_loss: nil, usd_value: nil, value: nil, timestamp: nil, block_height: 10, aggregates: %{}, calculations: %{}, order: nil, ... >}``` And then the next time I did the update, I passed in the old wallet variable. So I'm assuming the logs I see here, are not a return of the updated data from the db, but rather just showing what the update will look like?
ZachDaniel
ZachDaniel2y ago
I might have lost track of the specific issue. It sounds like things are working properly? If you pass in a record to be updated though, and attributes are selected, it should not come back w/ nil on an update.
obsidian
obsidianOP2y ago
Yea I guess the order is: First update of wallet, setting
bought_usd_volume
bought_usd_volume
bought_volume
bought_volume
```elixir
iex(39)> Leaderboard.Data.Wallet.update_stats(wallet, stats)

commit []
{:ok,
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: 1.0,
bought_volume: 2.0,
holdings_count: 3,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: nil,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>}
```elixir
iex(39)> Leaderboard.Data.Wallet.update_stats(wallet, stats)

commit []
{:ok,
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: 1.0,
bought_volume: 2.0,
holdings_count: 3,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: nil,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>}
Second update of wallet: Just sets usd_value.
iex(45)> new_stats
%{usd_value: 2000.0}
iex(46)> Leaderboard.Data.Wallet.update_stats(wallet, new_stats)
commit []
{:ok,
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: nil,
bought_volume: nil,
holdings_count: nil,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>}
iex(45)> new_stats
%{usd_value: 2000.0}
iex(46)> Leaderboard.Data.Wallet.update_stats(wallet, new_stats)
commit []
{:ok,
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: nil,
bought_volume: nil,
holdings_count: nil,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>}
As you can see, the previous bought_usd_volume etc... is now nil. I'm assuming this is because I'm passing in the original wallet before the update. So the logs that I'm seeing are not a return of the wallet object on a round trip from the db. But rather it is just logging what the change will look like Because when I query the wallet again, I get all the updates:
iex(49)> {:ok, [wallet]} = Leaderboard.Data.Wallet.find_wallet("SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks)

["SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks]
{:ok,
[
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: 1.0,
bought_volume: 2.0,
holdings_count: 3,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>
]}
iex(49)> {:ok, [wallet]} = Leaderboard.Data.Wallet.find_wallet("SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks)

["SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF", :stacks]
{:ok,
[
#Leaderboard.Data.Wallet<
__meta__: #Ecto.Schema.Metadata<:loaded, "wallets">,
id: "318fd4a8-5a4d-47d5-80c2-613207a3d6fc",
address: "SPAX2SZCDFTVV76SR4JY4RYEPC5PBH2QAHEJXHTF",
chain: :stacks,
bought_usd_volume: 1.0,
bought_volume: 2.0,
holdings_count: 3,
holdings_listed_count: nil,
realized_profit_loss: nil,
realized_usd_profit_loss: nil,
sold_usd_volume: nil,
sold_volume: nil,
unrealized_profit_loss: nil,
unrealized_usd_profit_loss: nil,
usd_value: 2000.0,
value: nil,
timestamp: nil,
block_height: 10,
aggregates: %{},
calculations: %{},
__order__: nil,
...
>
]}
ZachDaniel
ZachDaniel2y ago
not sure what the logs are that you're talking about. If you're talking about the SQL logger it just logs the sql statements, nothing more/less. But yeah, you likely want to pass the wallet through each time you're right that that is the reason for nil values in the second update
obsidian
obsidianOP2y ago
Ok great! Really appreciate the help Zach
ZachDaniel
ZachDaniel2y ago
my pleasure 😄

Did you find this page helpful?