ariadne icon indicating copy to clipboard operation
ariadne copied to clipboard

Get the list of requested output fields

Open suriyanto opened this issue 4 years ago • 4 comments

I'm using Ariadne to allow generic query on top of our flat database through SQL statements. As some of the tables could have up to 500 fields, for performance purposes, I would like to be able to know which output fields are requested so I won't be loading all the unnecessary bytes.

According to #218, I should be able to get the requested output fields through info.field_nodes, but I only get the top query field name as the only entry here. Below is some test code to reproduce.

type_defs = gql("""
    type Query {
        user(name: String!): [User]
    }

    type User {
        firstName: String
        lastName: String
        age: Int
    }
""")

@query.field("user")
def resolve_user(_, info, name):
    print('len = {}'.format(len(info.field_nodes)))
    fields = [f.name.value for f in info.field_nodes]
    print('fields = {}'.format(fields))
    return [{'firstName': 'Bob'}, {'firstName': 'Alan'}]

When I query using below:

{
  user(name: "Bob") {
    firstName
    lastName
  }
}

I get both firstName and lastName in the response output, but the printout gives me just the following:

len = 1
fields = ['user']

I'm using Ariadne 0.9.0.

suriyanto avatar Dec 29 '19 18:12 suriyanto

See this thread on Spectrum: https://spectrum.chat/ariadne/general/lazy-resolve-of-fields~ca75fa83-1fc3-4ecd-9c5b-36e96615b640

patrys avatar Jan 07 '20 15:01 patrys

Marked it for docs - we glance over the info contents, but it may be worth it to mention somewhere how to introspect it to find out what fields are queried for if you need it for prefetching implementation.

rafalp avatar Jan 10 '20 21:01 rafalp

Thanks for the info. I found that the info object is not easy to deal with. For documentation, might be useful to also give a sample function on how to extract. Here's what I use.

def collect_predicate_names(selections):
    predicates = []
    for i in range(len(selections)):
        if (isinstance(selections[i], InlineFragmentNode)):
            predicates.extend(_collect_predicate_names(selections[i].selection_set.selections))
        else:
            predicates.append(selections[i].name.value)

    return predicates

Let me know if I can help on the doc and if there's a general guidelines.

suriyanto avatar Jan 12 '20 21:01 suriyanto

Building on @suriyanto's solution, here is what I've come up with so far (Python 3.10+, but easily adaptable to older versions):

def selected_fields(info: GraphQLResolveInfo) -> list[str]:
    names: list[str] = []
    for node in info.field_nodes:
        if node.selection_set is None:
            continue
        names.extend(
            _fields_from_selections(info, node.selection_set.selections)
        )
    return names


def _fields_from_selections(
    info: GraphQLResolveInfo, selections: Iterable[SelectionNode]
) -> list[str]:
    names: list[str] = []
    for selection in selections:
        match selection:
            case FieldNode():
                names.append(selection.name.value)
            case InlineFragmentNode():
                names.extend(
                    _fields_from_selections(
                        info, selection.selection_set.selections
                    )
                )
            case FragmentSpreadNode():
                fragment = info.fragments[selection.name.value]
                names.extend(
                    _fields_from_selections(
                        info, fragment.selection_set.selections
                    )
                )
            case _:
                raise NotImplementedError(
                    f"field type {type(selection)} not supported"
                )
    return names

It would be great if we could get such a utility function/method into ariadne proper.

srittau avatar Jul 29 '22 14:07 srittau

If you want go deeper:

from collections.abc import Iterable

from graphql import GraphQLResolveInfo
from graphql.language import (
    FieldNode,
    SelectionNode,
    InlineFragmentNode,
    FragmentSpreadNode,
)

def selected_fields(info: GraphQLResolveInfo) -> list[str]:
    names: list[str] = []
    for node in info.field_nodes:
        if node.selection_set is None:
            continue
        names.extend(_fields_from_selections(info, node.selection_set.selections))
    return set(names)


def _fields_from_selections(
    info: GraphQLResolveInfo, selections: Iterable[SelectionNode]
) -> set[str]:
    names: list[str] = []
    for selection in selections:
        match selection:
            case FieldNode():
                names.append(selection.name.value)
                
                # go deeper to the next level
                if selection.selection_set is not None:
                    names.extend(
                        _fields_from_selections(
                            info, selection.selection_set.selections
                        )
                    )
                # ---

            case InlineFragmentNode():
                names.extend(
                    _fields_from_selections(info, selection.selection_set.selections)
                )
            case FragmentSpreadNode():
                fragment = info.fragments[selection.name.value]
                names.extend(
                    _fields_from_selections(info, fragment.selection_set.selections)
                )
            case _:
                raise NotImplementedError(f"field type {type(selection)} not supported")
    return names

andryushka avatar Feb 09 '23 12:02 andryushka