Skip to content

Conversation

@andimarek
Copy link
Member

@andimarek andimarek commented Jun 21, 2020

This PR makes the NormalizedField from the normalized query available to a DataFetcher.

This allows for example to look ahead in a DataFetcher including Fragments on Interfaces on Unions, which is currently not possible.

@andimarek andimarek changed the title make normalized field available to a DFE make normalized field available to a DataFetcher Jun 22, 2020
@andimarek andimarek requested a review from bbakerman June 22, 2020 00:22
private final GraphQLObjectType objectType;
private final GraphQLFieldDefinition fieldDefinition;
private final List<NormalizedField> children;
private final boolean isConditional;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can there be directives on a NormalizedField?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indirectly yes: Every normalized field maps to a merged Field.
See https://github.com/graphql-java/graphql-java/pull/1963/files#diff-844886aefd16bdb049a548e3273cf543R18. And every MergedField has a list of Field, which again might have a directive. This is for query directives.

For schema directives you can look directly at the GraphQLFieldDefinition.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw: The reason why all Ast elements are kept in a separate map is that a NormalizedQuery is independent of the Ast of the Query.

There are infinite different Queries which lead to the same NormalizedQuery.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say "infinite" it's because of merged fields, right?
This:

fragment mergeIdenticalFields on Dog {
  name
  name
}

fragment mergeIdenticalAliasesAndFields on Dog {
  otherName: name
  otherName: name
}

I was looking at what lookahead functionality graphql-js provide and it looks like they expose the AST data structures directly so without building a normalized view of it. GraphQLResolveInfo.fieldNodes

Copy link
Member Author

@andimarek andimarek Jun 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tinnou it is merged fields and fragments. The example you mentioned are the simple cases.

{pets{name name ... on Dog {name ...F ...F ...F3} ... on Cat {name ...F2}} 
fragment F on Pet {name} 
fragment F2 on Cat{name ... F} 
fragment F3 on Dog {...F name }

is the same as

{pets{name}}

GraphQL Java also provides the the Ast Fields (FieldNodes in GraphQL.js) in a DataFetcherEnvironment (getFields or getMergedField), but we found this is simply not enough, this is why we are adding NormalizedFields.

Copy link
Member Author

@andimarek andimarek Jun 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate a bit more: Because of this flexibility in writing your query the Ast is really not a good structure to lookahead (or for example to look back and ask for all directives declared on the parents).

For example an ast field inside a fragment can lead to n normalized fields:

{pets {...F friends @cachable {...F}}}
fragment F on Pet {
  name 
}

name is actually used in two different position of the Ast, so depending on if we are talking about the name of the friends or the name of the Pets we might have a directive @cachable declared on a parent or not. You can imagine how much more complex it can get with multiple fragments etc.

A normalized Query takes away all of this ambiguity and resolves all Fragments/Interfaces/Unions statically.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok your first example makes a lot of sense, I was trying to get info through merged field and I modified one of the unit tests to try to get at it and indeed, the field collector is incapable of "knowing" all the fields within the fragments are one and the same.

def "collect fields on inline fragments"() {
        def schema = TestUtil.schema("""
            type Query {
                pets: Pet
            }
            interface Pet {
              name: String
            }
            type Cat implements Pet {
              name: String
            }
            type Dog implements Pet {
              name: String
            }
                """)
        Document document = new Parser().parseDocument("""
            {pets{name @skip(if: false) name @include(if: true) ... on Dog {name ...F ...F ...F3} ... on Cat {name ...F2}}}
fragment F on Pet {name} 
fragment F2 on Cat{name ...F} 
fragment F3 on Dog {...F name }""")

        def object = schema.getType("Query") as GraphQLObjectType

        NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, null);

        FieldCollector fieldCollector = new FieldCollector()
        FieldCollectorParameters fieldCollectorParameters = newParameters()
                .schema(schema)
                .fragments(getOperationResult.fragmentsByName)
                .objectType(object)
                .build()


        Field pets = ((OperationDefinition) document.children[0]).selectionSet.selections[0] as Field

        when:
        def result = fieldCollector.collectFields(fieldCollectorParameters, mergedField(pets))

        then: "only the name fields on Pet interface were captured"
        result.subFields.get("name").fields.size() == 2
        result.subFields.get("name").fields.forEach {
            assert it.directives.size() > 0 && (it.directives.get(0).name == "skip" || it.directives.get(0).name == "include")
        }

I think now I grasp the full extent of the need to have the normalized query for looking ahead and having your example in the user documentation would be great actually. (as support for explaining the rationale)

For the part of looking back, to ask for the directives declared on the parent field, I want to make sure I understand correctly. In the example case, when you are executing the Pet.name data fetcher, one would like to know whether the parent field has the @cacheable directive?
i.e:

{
  pets { 
    name    <--- can ask whether @cacheable is present on pets
    friends @cachable {
      name  <--- can ask whether @cacheable is present on friends
    }
  }
}

So I guess if we didn't have the normalized query, looking at ExecutionStepInfo of the parent is not sufficient?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tinnou ExecutionStepInfo is sufficient if you strictly look back and if you only need to know the parent directives from a field inside a DataFetcher.

But keep in mind that it is a result (or execution) based structure: See this comment for a detail example how ESI looks like when you deal with lists. This means that when you traverse the parent hierarchy up in the ESI you need to deal with that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or to describe it from another angle:
NormalizedQuery is the best you can get without executing a Query: it lays between the Query AST and the actual execution.

@andimarek andimarek added this to the 16.0 milestone Jun 24, 2020
…d-to-dfe

# Conflicts:
#	src/main/java/graphql/util/WeakMemoizedSupplier.java
Copy link
Member

@bbakerman bbakerman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add extra helpers somewhere that makes it easier
to find out IF a given field is present say

Like DataEnvSelectionSet does - not 100% sure where that should sit or not

But this PR unlocks heaps of value so lets get it in

@jhaals
Copy link

jhaals commented Oct 8, 2020

We've tried this PR and it looks promising.
We currently do lookahead by doing this which work with fragments and the regular query.

    var names =
        dataFetchingEnvironment.getNormalizeField().getChildren().stream()
            .filter(normalizedField -> !normalizedField.getName().startsWith("__"))
            .filter(normalizedField -> normalizedField.getObjectType().getName().equals("User"))
            .map(NormalizedField::getName)
            .collect(Collectors.toList());

Thanks for jumping on this, we're looking forward to getting this released.

@bbakerman
Copy link
Member

We've tried this PR and it looks promising.
We currently do lookahead by doing this which work with fragments and the regular query.
Thanks for jumping on this, we're looking forward to getting this released.

The code there is great for single level field matching but we have things like

contains("invoices/payments/*")

in the data selection set today and I would love to get that in here some where

@jhaals
Copy link

jhaals commented Oct 9, 2020

The code there is great for single level field matching but we have things like

contains("invoices/payments/*")

in the data selection set today and I would love to get that in here some where

We have the same thing where we need to collect the field names on a deeper level.
We managed to get that to work too but it would be great to have a simpler API then having to go level by level and checking for existence. A feature to collect the deeper fields would be fantastic.

 collect("invoices/payments/*")

@jhaals
Copy link

jhaals commented Oct 15, 2020

@bbakerman @andimarek it would be great to get this in master as this is the only working solution for unions currently. We've started implementing against this branch and would love helpers like those suggested above but at even the bare functionality would be extremely useful. Perhaps the coming improvements/tweaks can come in separate PRs to unblock this

@bbakerman
Copy link
Member

Superseded by #2079

@bbakerman bbakerman closed this Oct 20, 2020
@andimarek andimarek deleted the add-normalized-field-to-dfe branch May 4, 2021 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants