Recursive Queries with Absinthe

Hey Guys,

I am trying a recursive GraphQL query, a la, which returns a tree of permissions,

query {
  permission(command: "base"){
    ...PermissionFields,
    children{
      ...PermissionFields,
      children{
        ...PermissionFields
      }
    }
  }
}

fragment PermissionFields on Permission {
  command,
  name
}

and getting a result like,

{
  "data": {
    "permission": {
      "name": "System Management",
      "command": "base",
      "children": [
        {
          "name": "User Management",
          "command": "base/usermanagement",
          "children": null
        },
        {
          "name": "Configuration",
          "command": "base/pluginconfigs/getconfig",
          "children": null
        },
        {
          "name": "Cron Jobs",
          "command": "base/cronjobs/index",
          "children": null
        },
...

whatever I try, I cannot seem to get more than one level of children in my result at a time.

I suspect this may be something wrong with my code (although when I manually re-query at each level, everything looks fine…)

But just on the off chance - is there something inherent in Absinthe that doesn’t like recursive queries like this?

Are there any recursion examples I can look at?

Hey! To really provide an answer I’ll need to see the relevant parts of your schema and resolvers, but I can make a few points here prior.

Most directly to your question the fact that the second layer has "children": null means that something likely at the resolver level isn’t successfully pulling up the data. If you can provide your permission object and the child field resolver we can dig into that.

More abstractly, what you’ve got here technically isn’t recursion. You do specify several layers the same in the document but nothing strictly speaking calls itself. An example of recursion would be:

fragment PermissionFields on Permission {
  command,
  name
  children {
    ... PermissionFields
  }
}

And this is not permitted.

1 Like

Yeh, I know about the recursion limitation, I think it’s a smart limitation actually, pity it’s not more widespread…

Good about using the term ‘recursion’ though - I suppose this is a solution which mimics what I would normally do with recursion : )

Tangential ; )

In any case, here’s the schema, object and resolver info.

(Thanks again Ben - super helpful : )))

Schema:

    @desc "Get a permission"
    field :permission, type: :permission do
      arg :command, non_null :string
      resolve (AllIAskWeb.Utils.handle_errors &AllIAskWeb.PermissionResolver.find/2)
    end

Resolver:

  def find(%{command: command}, _info) do
    case AllIAsk.Repo.get AllIAsk.Wsp3.Permission, command do
      nil  -> {:error, "Permission command #{command} not found"}
      permission ->
        permission_ = Map.put permission, :children, (children %{command: permission.command}, :a)
        {:ok, permission_}
    end
  end
  
  def children(%{command: command}, _info), do:
    AllIAsk.Wsp3.Permission
    |> (where parent: ^command)
    |> AllIAsk.Repo.all

Object:

  object :permission do
    field :command, non_null :non_empty_string
    field :name, non_null :non_empty_string
    field :description, non_null :non_empty_string
    field :deny_on_cs_client, non_null :boolean
    field :sort, :integer
    field :menus_id, non_null :string
    field :parent, non_null :string
    field :grant_to_tenant, non_null :boolean
    field :default_license, non_null :string
    field :children, list_of :permission
  end

Oh - I think I have it, the children function doesn’t also retrieve the children’s children. Perhaps a trivial fix - thanks for putting me on the right track : )

Just for completeness, this change makes everything OK. Not super efficient, but I am sort of constrained by DB table… Thanks again Ben!

def children(%{command: command}, _info), do:
    AllIAsk.Wsp3.Permission
    |> (where parent: ^command)
    |> AllIAsk.Repo.all
    |> (Enum.map (
        fn(p) ->
          Map.put p, :children, (children %{command: p.command}, :a)
        end
        )
        )

Well, that only gets you so far.

If you asked for another layer this wouldn’t work. The general solution here is to have a resolver on the children field that gets the children for a permission. Then no matter how many layers you want to do, it, it’ll load the children. This has the nice perk that if you DONT ask for any children to start with, none get loaded.

You may be concerned that this causes N+1 queries, and it does if you have the resolver actually do the Repo.all call. Enter https://hexdocs.pm/absinthe_ecto/Absinthe.Ecto.html

http://absinthe-graphql.org/guides/ecto-best-practices/ explains the motivations a bit more, although is slightly outdated with respect to what available solutions exist.

2 Likes

Currently, I can ask for as many layers as I want, it seems (5 and counting)…

Never thought about putting a specific resolver on the children field however, will give that a spin also - suspect way more efficient that my solution…

Thanks for the links! Really exciting library you have here : )