ash
ash copied to clipboard
Allow to specify a read action for a relationship in a load
This is a feature request for a convenient way to specify a read action for a load of a relationship.
Example: Let's say we have a blog with Post and User. Post has belongs_to :author, User. And there are two relevant pages - a page with a list of all posts and a page for a single post. In both cases when posts are read we want to load their authors too. But in a list an author is represented only by a name while on a post page we want to show their avatar, small bio/about paragraph, maybe posts count, stuff like that.
The problem is that currently there is no simple way to do it.
A possible solution is to allow passing action name in load with syntax equal to that for calculations:
query |> load(relationship: :action)
query |> load(relationship: [action: args])
query |> load(relationship: [action: {args, nested_loads}])
Thoughts?
You can currently do load(relationship: Ash.Query.for_read(Resource, :action)), IIRC. Would need to test how that behavior plays with the current logic, as at the moment relationships are explicitly configured with an action.
i.e
relationships do
has_many :foos, Foo do
read_action :foo
end
end
You can also configure two actions w/ different names that use a different read action
I'm mostly concerned about the syntax of it, as the options you showed are not unambiguous (if you had a relationship or calculation called the same name as the action)
load(relationship: Ash.Query.for_read(Resource, :action))
I thought about it. But will it work inside resource actions? I mean inside calls to build preparation, I assume there might be a problem with compilation order because when for_read is called the other resource is not yet compiled and then the only way is to do it in a custom preparation (a function or a module).
configure two actions w/ different names
You mean two relationships, right? Yeah, currently this is the only way to properly use different actions. It is verbose though, need to declare relationship for each different read action (and come up with a name convention for that).
I'm mostly concerned about the syntax of it
Yeah, I am open to alternatives, that is just an option.
if you had a relationship or calculation called the same name as the action
True, but that applies to all of them - current resolution order is "relationship > calculation > attribute". It will just add "> action" at the end to be non-breaking change.
but you can't have a relationship or calculation or attribute with the same name. I think if we want to support it we'd need to come up with some kind of non-ambuguous name. We have a reserved name :as, that can be used to load things with a different name i.e relationship: [as: :foo, ...]. Perhaps we could repurpose this, i.e with as: {:name, :action}, but even that is a bit messy. I'm open to the idea, just need unambiguous syntax.
but you can't have a relationship or calculation or attribute with the same name.
Well, Ash 3 is coming and and I don't see why action names cannot be introduced into that uniqueness check. I assume words like read, create, update, delete, get and so on are not really used for those other things, so there won't be many collisions.
Hm....that is true. But is this particular feature worth the introduction of that requirement? For instance, those other things can't share a name because they live in the same map, it's a technical impossibility for them to share a name. I think I'd want to see something like what you laid out, or perhaps even a new pattern around loading things that is cleaner even, like [relationship_name: Ash.Load.action(:action)] or something (thats not the right answer, just making something up).
Yeah, same, I don't know the perfect solution at the moment.
Another thing which I thought about adding possibly later and for more generic purposes is to accept functions to customize a relationship query:
query |> load(relationship: customize)
query |> load(relationship: {customize, nested_loads})
Where customize accepts and returns a query. (For example for_read inside action definition might not work because it will not have access to actor and other query opts, but with customize those opts can be present and read from the input query.)
Another option (again, just throwing things at the board) is to use tuples:
query |> load(relationship: {:action})
query |> load(relationship: {:action, args})
query |> load(relationship: {:action, args, nested_loads})
This should be unambiguous. But I'm not sure about the look (nothing against, just not sure).
Or with a map:
query |> load(relationship: %{action: :action})
query |> load(relationship: %{action: :action, args: args})
query |> load(relationship: %{action: :action, args: args, load: nested_loads})
I think map is better as it allows adding options later like build, customize and omitting some of them like load without args.
Yeah, a map could work. Currently if you see a map in a load statement it can only be calculation arguments I believe. I'm open to it 👍