How to apply filtering on a relationship based on the read action of the parent

How do I apply a specific filtering on a related resource that’s two layers down from the parent resource, where the filter is determined from the parent’s read action being called?

Here’s the AshGraphql query:

query GetMember($id: ID!) {
  getMember(id: $id) {
    id
    profile {
      answers {
        id
        visible
      }
    }
  }
}

The Ash resources involved:

  • Member
    • → has_one Profile
      • → has_many Answers

For this particular GraphQL query, the answers should be filtered by visible=true, where visible is an attribute of Answer. The filtering should only apply to this particular action (:get_member), not any other actions.

Generally speaking this kind of thing is kind of difficult to do. It also causes problems for generic tooling that might think it can do things like cache any given profile that it sees in a gql response.

You could accomplish it with a relatively complex munging of the load statement in a before action hook on the action in question, which if you do go this route I can help you accomplish. You have to account for the fact that loading relationships for ash_graphql sometimes uses a calculation so that it can load the same relationship multiple ways.

We could also potentially add some kind of dedicated tool for preprocessing included relationships based on the action.

With all that said, I’d be very curious to know why you want this and if there are any other alternatives that might work for you. Resolving instances of a given type differently depending on their location in the tree is IMO an anti pattern that can cause problems later down the line.

1 Like

Got it. It sounds difficult and probably not worth doing using standard techniques.

I could also solve this on the client side. In fact, that’s exactly what I ended up doing. It doesn’t feel very clean, but it doesn’t seem terrible either. The server side still filters out profile answers from those who should not have access to it, but in the end, when I’m viewing my own public profile, I don’t see my hidden answers on the page, even though the data is there in the API response.

Another approach I considered is having two different relationships from Profile to Answer: public_answers, and answers (unfiltered). I might go back and change the application to do that instead.

To answer your question about why my application has this data particular access requirement, these are the user stories:

  1. As a registered member, when I see my profile page (get_member action), I want it to look exactly to me as other members see it. If an answer (field) in my profile is invisible to others, then it should be invisible to me too. However, when I edit my profile (get_self_member action), I should be able see see all answers, visible or not.
  2. As an admin member, when I see another members’s profile page (get_member action), I want it to look exactly to me as other members see it. If an answer (field) in the profile is invisible to others, then it should be invisible to me too. However, when I see the admin view of the member’s profile (admin_get_member action), I should be able see see all answers, visible or not.

Is there no way to rewrite the query like this?

query GetMember($id: ID!, $visible: Boolean!) {
  getMember(id: $id) {
    id
    profile(visible: $visible) {
      answers {
        id
        visible
      }
    }
  }
}

In that case, I assume $visible could be used as an argument somewhere? I have never used Ash with Graqphl and I’m quite new to Ash but I have the feeling that it might be easier to implement?

1 Like

You can definitely do it out of the box, i.e something like this:

query GetMember($id: ID!) {
  getMember(id: $id) {
    id
    profile(filter: {visible: {eq: true}}) {
      answers {
        id
        visible
      }
    }
  }
}

With that said, I think that @moxley’s use case is actually quite interesting. Masquerading ought to be, I think, its own concept. I might suggest setting up an actual masquerading concept. For instance, you could add a masquerading_user calculation to your user resource that returns nil, and populate that field in a plug using a header. That header would tell your system to do something like "if the user is allowed to masquerade as the user they are requesting to masquerade as, then set the actor to %{that_user | masquerading_user: the_admin}.

Then the relevant user’s policies will automatically apply for that request.

1 Like

Yes, I believe that would work! It would be more like this:

query GetMember($id: ID!) {
  getMember(id: $id) {
    id
    profile {
      answers(filter: {visible: {eq: true}}) {
        id
        visible
      }
    }
  }
}

Because the visible attribute is on the Answer resource.

Of course, you still want to filter the actual attribute using policies if users are not supposed to see it.