vektah / gqlparser

A port of the parser from graphql-js into golang
MIT License
501 stars 123 forks source link

Pass down ast.Path of current `field`, `argument` etc.. inside validator.Events.OnField #332

Open shotmk opened 2 weeks ago

shotmk commented 2 weeks ago

GraphQL errors have a path field that helps clients to locate the path in graph where error happened.

I am writing some custom directive implementation on top of validator.Walk and validator.Events APIs inside gqlparser.

I was surprised to find out that i do not have access to current field ast.Path inside the OnField() callback.

Looking on a walker implementation it feels like it is pretty easy to calculate path as we are doing a straightforward tree traversal.

Can ast.Path info be added to the OnField Events listener of validator.Events ?

StevenACoffman commented 2 weeks ago

@shotmk I would always be very open to a PR that helps clients to locate the path in graph where error happened, especially if it did not break backwards compatibility or cause a big performance regression.

Are you suggesting that the ast.Path would be on the walker or the Events, or on the Field or somewhere else?

It's currently only in the varValidator

shotmk commented 1 week ago

@StevenACoffman I think ast.Path fits to be inside ast.Field next to Position field. (as position and path logically have the same purpose: locating the field in document / schema).

StevenACoffman commented 1 week ago

Sure, that makes sense. Are you able to make a PR for that? I'm swamped at work for the foreseeable future, but would like to see this happen.

shotmk commented 1 week ago

@StevenACoffman I will try to find time for a PR in near future ;)

shotmk commented 4 days ago

@StevenACoffman I am working on the PR and noticed a weird behavior of OnField observer. I have a schema like this:


type Query {
 topLevelField: TopLevelField
}

type TopLevelField {
 nestedField: NestedField
}

type NestedField {
 deeplyNestedField: String
}

schema { query: Query}

and i am doing the following query:


{
 topLevelField {
   nestedField {
     ...NestedFieldFragment
   }
 }
}

fragment NestedFieldFragment on NestedField {
 deeplyNestedField
}

In my test i am doing :

called := 0
observers := &Events{}
observers.OnField(func(walker *Walker, field *ast.Field) {
  called++
})
Walk(schema, query, observers)
require.Equal(t,3, called)

Expecting the OnField to be called 3 times, but actually it is called 4 times. This happens because how walk() is being implemented in walk.go

    for _, child := range w.Document.Operations {
        w.validatedFragmentSpreads = make(map[string]bool)
        w.walkOperation(child) 
        // AT THIS POINT OnField is called 3 times 
    }
    for _, child := range w.Document.Fragments {
        w.validatedFragmentSpreads = make(map[string]bool)
        w.walkFragment(child)
        // This additional walkFragment causes the additional OnField call
    }

Is it expected can this be changed ? Now when i am adding field paths, i can keep track on called unique paths inside the walker and make sure to trigger OnField for each unique field path of the operation exactly once.

WDYT?

StevenACoffman commented 4 days ago

That might be great! I remember there were some tricky corner cases where it was better to call some observers twice to avoid missing calling other observers at all. I can't think of why we would need to call the observers twice.

Looking at this on my phone on a bus makes me think something else might also be incorrect.

For example, In the walk() it looks like we should maybe not reset the validatedFragmentSpreads and instead have walk() be:

    w.validatedFragmentSpreads = make(map[string]bool)
    for _, child := range w.Document.Operations {
        w.walkOperation(child)
    }
    for _, child := range w.Document.Fragments {
        w.walkFragment(child)
    }

WDYT?

I'd need to look at the side effect differences between walkFragment and walkOperation. I seem to recall that walkOperation would do w.walkDirectives(def, operation.Directives, loc) with loc being a ast.LocationQuery, ast.LocationMutation, or ast.LocationSubscription, but that walkFragment instead calls w.walkDirectives(def, it.Directives, ast.LocationFragmentDefinition). I forget what practical difference that makes and how the validatedFragmentSpreads interacts.

Sorry, this is my bus stop! :)