ash icon indicating copy to clipboard operation
ash copied to clipboard

Allow to specify a read action for a relationship in a load

Open vonagam opened this issue 2 years ago • 10 comments

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?

vonagam avatar Sep 27 '23 16:09 vonagam

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

zachdaniel avatar Sep 27 '23 16:09 zachdaniel

You can also configure two actions w/ different names that use a different read action

zachdaniel avatar Sep 27 '23 16:09 zachdaniel

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)

zachdaniel avatar Sep 27 '23 16:09 zachdaniel

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.

vonagam avatar Sep 27 '23 16:09 vonagam

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.

zachdaniel avatar Sep 27 '23 18:09 zachdaniel

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.

vonagam avatar Sep 27 '23 18:09 vonagam

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).

zachdaniel avatar Sep 27 '23 19:09 zachdaniel

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.)

vonagam avatar Sep 27 '23 19:09 vonagam

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.

vonagam avatar Sep 27 '23 19:09 vonagam

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 👍

zachdaniel avatar Sep 27 '23 19:09 zachdaniel