99designs / gqlgen

go generate based graphql server library
https://gqlgen.com
MIT License
9.96k stars 1.17k forks source link

How do I manipulate the context at root field level but after the root field resolver runs #2650

Open smilence-yu opened 1 year ago

smilence-yu commented 1 year ago

What happened?

I'm trying to inject a token into the golang context for each request, but only under certain condition based on the query. Because the token is only needed for loading certain GraphQL entities. On the other hand, I don't want to have this middleware to be applied using AroundFields either because if there are multiple such entities, i don't want to query the token multiple times which is expensive. i only want to query the token once.

So I wrote the following code and used AroundRootFields. But because this middleware runs before the RootResolver executes, CollectFields method will return me empty slice as nothing has been resolved.

srv.AroundRootFields(s.tokenMiddleware)

func (s *Server) tokenMiddleware(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
    oc := graphql.GetOperationContext(ctx)
        collected := graphql.CollectFieldsCtx(ctx, nil) // returns me empty slice
    fields := getFieldsRecursively(
           oc, 
           collected,
        ) // get back a slice of strings for the fields but this won't work

        if !lo.Contains(fields, "specific_field_name") {
          return next(oc)
        }

    token := s.getToken(ctx)
    if token == nil {
        return next(ctx)
    }
    return next(context.WithValue(ctx, "access_token", token))
}

What did you expect?

Ideally I can have something like PostRootResolverMiddleware which executes only once per root field after root resolver runs but before passing context to the sub-field resolvers.

If this is not supported atm but shouldn't be too hard to implement, I can try putting out a PR too.

Minimal graphql.schema and models to reproduce

versions

ShawnMilo commented 1 year ago

I also would love to know a clean way to do this.

In the meantime, here's the workaround I use which may also work for your situation. It intercepts the request and adds values to the context one time per request.

http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
    sessionID = r.Header.Get("session-id")
    ctx = context.WithValue(ctx, "session_id", sessionID)
    userUID := lookupSession(ctx, sessionID)
    ctx = context.WithValue(ctx, "user_uid", userUID)
    requestID := ksuid.New().String()
    ctx = context.WithValue(ctx, "request_id", requestID)

    r = r.WithContext(ctx)
    gqlServer.ServeHTTP(w, r)
})

If it's possible to know which assets were queried this would be more useful for you. If you know how to make that work (or the core devs can explain the "proper" solution), I'd be grateful!

smilence-yu commented 1 year ago

I also would love to know a clean way to do this.

In the meantime, here's the workaround I use which may also work for your situation. It intercepts the request and adds values to the context one time per request.

http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
    sessionID = r.Header.Get("session-id")
    ctx = context.WithValue(ctx, "session_id", sessionID)
    userUID := lookupSession(ctx, sessionID)
    ctx = context.WithValue(ctx, "user_uid", userUID)
    requestID := ksuid.New().String()
    ctx = context.WithValue(ctx, "request_id", requestID)

    r = r.WithContext(ctx)
    gqlServer.ServeHTTP(w, r)
})

If it's possible to know which assets were queried this would be more useful for you. If you know how to make that work (or the core devs can explain the "proper" solution), I'd be grateful!

@ShawnMilo thx. yes exactly as you mentioned, what you have proposed here applies to use cases where i always want to inject a token, but given the query hasn't been resolved, I won't be able to inject a token based on the resolved query.