google / rejoiner

Generates a unified GraphQL schema from gRPC microservices and other Protobuf sources
https://google.github.io/rejoiner/
Apache License 2.0
3.67k stars 144 forks source link

Question: How would rejoiner support getting grandparent context? #56

Open xunnanxu opened 5 years ago

xunnanxu commented 5 years ago

In the js counterpart, there's a way to add arbitrary context info to a resolve context and thus you can relay the information all the way down as described in this issue: https://github.com/graphql/graphql-js/issues/1098

However in rejoiner since all fields are strictly typed to proto msgs or java native types, and there's no resolver exposed, it doesn't seem that use case is supported, unless one flattens the grandparent context onto the proto message (e.g. when schema is foo -> bar -> baz, let baz have foo's id), which is doable but doesn't seem like a good practice?

siderakis commented 5 years ago

It's possible to get the DataFetchingEnvironment (graphql.schema.DataFetchingEnvironment) in any rejoiner method (query/mutation/modification). This can be used to access a "context" object that is set when the query is executed.

Here the context is set: https://github.com/google/rejoiner/blob/master/examples/src/main/java/com/google/api/graphql/examples/streaming/graphqlserver/GraphQlGrpcServer.java#L113

Here the DataFetchingEnvironment in injected in (it contains the context).

https://github.com/google/rejoiner/blob/master/examples/src/main/java/com/google/api/graphql/examples/streaming/graphqlserver/HelloWorldSchemaModule.java#L35

You can think of rejoiner methods as datafetchers.

I'm not 100% sure what you are trying to do, does this help?

xunnanxu commented 5 years ago

Hi @siderakis thanks for the quick reply. I think unfortunately the answer is no. Using https://graphql.org/learn/execution/#root-fields-resolvers as a reference, the context object here will apply to all resolvers whereas the grandparent id I mentioned needs to be resolver specific (i.e. different values applied to different resolvers). The solution I originally referenced is achieved by enhancing the obj param in resolver, and that translated to rejoiner language means to dynamically add fields to proto message such that deeply nested object can get ids more than one level above through @SchemaModification. This does not seem achievable due to rejoiner only allows proto message to be returned not some interim custom data structure.

As an example assuming the query is like

allPeople(startDate, endDate) {
  company {
    id
    department {
      id
      team {
        id
        employee {
          id
        }
      }
    }
  }
}

And let's assume each level is 1-many. The context solution can pass startDate and endDate to each layer because they are all the same. But in case employee needs department id for grpc call that becomes tricky (assuming employee proto only has team id and team has department id). We cannot use the global context because different employee would belong to different department.

siderakis commented 5 years ago

In this case the employee field depends on team and department. If there was another way to reach the team proto without having department as a parent employee wouldn't be resolvable, right?

The best thing I can think of right now is to replace the team field with a TeamAndDepartment proto. That proto would have two fields, one for the team and one for the department. You could then remove the department field from the schema.

allPeople(startDate, endDate) {
  company {
    id
    department {
      id
      teamAndDepartment {
        id
        employee {
          id
        }
      }
    }
  }
}

Sorry it's not a great solution.

Maybe a new return type could added ProtoAndContext.

class ProtoAndContext<M extends message> {
 M protoMessage;
 Object context;
}

In this case the framework would only use the protoMessage field for the schema and could supply the context to downstream "resolvers".

Would this solve the general case, or just the example you gave?

xunnanxu commented 5 years ago

Yeah but that bloats the return schema, which makes it messy (or you'd have to somehow remove the department id at team level to avoid that being returned to user). ProtoAndContext might work but that defeats the strong typing purpose. I wonder if it makes sense to allow methods to return arbitrary object that wraps a proto msg like

interface EnhancedContext<T extends Message> {
  T getMessage();
}

that way rejoiner can call that method to behave the same way today (and throws an exception if it can't). It's pretty much what you proposed but users would need to define their class rather than having Object as the type (or you could probably use generic for context).