Hi everyone. I'm trying to do some schema stitchin...
# graphql
h
Hi everyone. I'm trying to do some schema stitching using Apollo Gateway; I am able to consume both remote schemas, I also have definitions for schema extensions and resolvers. Basically, I am trying to retrieve the school "object" for a student, by using the 
schoolId
. The catch being that the Student schema and School schema are in separate services. Not sure what I'm missing. I am unable to "join" both types using the resolver. Can you kindly help me out? Here´s the code:
*const* VCDB_URI = '<http://localhost:4001/graphql>'
*const* PCDB_URI = '<http://localhost:4002/graphql>'
//Define schema extensions
`*const* typeDefs = ``   
extend type Student {
    
schoolId: School
  
}
``;`
*const* resolvers = {
  
Student: {
      
schoolId(parent, args, context, info) {
        
return delegateToSchema({
          
schema: typeDefs,
          
operation: 'query',
          
fieldName: 'getBySchoolId',
          
args: {
            
schoolId: parent.id,
          
},
          
context,
          
info,
        
});
      
}
  
}
}
//Define the gateway
*const* gateway = new ApolloGateway({
  
serviceList: [
    
{ name: 'vcdb', url: VCDB_URI },
    
{ name: 'pcdb', url: PCDB_URI },
  
],
  
extensions: typeDefs,
  
resolvers: resolvers,
  
debug: true
});
//Define the server
*const* server = new ApolloServer(
  
{ gateway,
      
subscriptions: false,
   
});
//Start up the server
server.listen({
  
port: 4000,
*}).then(({ url }) =>* {
  `*console.log(
*:rocket:  *Server ready at ${url}
);*`
});
m
Hi 👋 Given this is a Kotlin slack, you might have more luck in the GraphQL community: https://graphql.org/community/
h
I'll check them out. Thank you.
d
yeah spectrum would get you the answer the fastest
that being said it looks like you are trying to do schema stitching -> there are number of tutorials available for it, e.g. see https://www.graphql-tools.com/docs/stitch-schema-extensions
you might want to consider moving towards Apollo Federation which removes the need for having this logic on the gateway and allows you to define your relationships programmatically within the underlying services
i.e. your schema that contains
school
resolvers would extend the
student
with additional
school
field
h
The services that contain the
school
and
student
entities are already federated, but they are separate services. I'm not sure how they would be able to resolve each others' type without having to be in the same service. Hence I decided to put that logic in the gateway itself, but that approach isn't working either. It's not showing any errors at startup either, it's just not doing the stitching.
h
I forgot to mention we are exposing the federated services with a combination of spring boot and expedia groups's jars, using Kotlin as well.
Only the gateway is using node.js
d
if you are using
graphql-kotlin
then take a look at https://expediagroup.github.io/graphql-kotlin/docs/federated/apollo-federation
h
yes, I think I can find what I need there. Thank you so much Dariusz!
d
also an FYI when we switched from stitching to apollo federation it resulted in pretty significant performance improvement in the gateway so besides keeping the gateway dumb it also made it faster
h
yes, we are trying to prevent overfetching, so this might be the best solution. I am taking a closer look at the extended schema example seen here: https://expediagroup.github.io/graphql-kotlin/docs/federated/federated-schemas
and in the Product definition it's using a method
getReviewByProductId(id)
, I assume this method is the resolver for the Review, correct? and it's all part of the same project and service? The problem we are facing is that we are trying to fetch from a different project and service, so I'm not sure how to wire that up, if at all possible.
d
federation tldr • service A knows about
Product
and how to fetch it • service B knows about
Reviews
and how to fetch it, it “knows” that
Product
type exists somewhere so it just adds new field to it to fetch reviews (as it subject matter expert on how to fetch reviews)
h
Hi Dariusz, sorry to bother again with this topic. I think I've figured most of it, but for some reason I am unable to fetch both values from my School Entity so I've added an
@ExternalDirective
annotation to schoolName. But when I do that, the gateway errors out with this message:
This data graph is missing a valid configuration. [vcdb] School.schoolName -> is marked as @external but is not used by a @requires, @key, or @provides directive.
This is is a diagram of the code I am using:
When I remove the
@ExternalDirective
from schoolName in service B, the gateway starts fine and I can retrieve
Student -> School -> schoolId
But for some reason
Student -> School -> schoolName
won't work
d
do you have a sample github project that shows this behavior?
basically i think in your example it should be reversed • service A defines
School
and extends
Student
with additional
school
field • service B defines
Student
i.e. since service A is subject matter expert on
School
it should have all the logic related to fetching that data
h
Unfortunately nothing I can publish on github, but I will try with the approach you are mentioning. Thank you
Hello again Dariusz, I've tried the approach you mentioned yesterday, now I am getting the following error when querying the gateway:
Service A schema looks like this:
Service B schema looks like:
Any help you could provide is greatly appreciated
Hi @Dariusz Kuc, sorry to be such a bother but I'm still struggling with the scenario above. Any input you can provide will be valuable.
d
your schemas look correct so the issue might be with your entity resolver
h
I suppose you mean the resolver for School in the Student extension correct? For now I am using a hardcoded object, but without success:
Copy code
@KeyDirective(fields = FieldSet("studentId"))
@ExtendsDirective
class Student(@ExternalDirective val studentId: Long,
              @GraphQLIgnore val schoolId: Long
){
    fun school(): School? = School(99,"Hardcoded")
}
d
h
I was indeed missing that resolver. I have created it like this:
Copy code
@Component
class StudentResolver(private val schoolRepository: SchoolRepository) : FederatedTypeResolver<Student> {

    override suspend fun resolve(environment: DataFetchingEnvironment, representations: List<Map<String, Any>>): List<Student?> =
        representations.map {

        val studentId = it["studentId"]?.toString()?.toLongOrNull() ?: throw InvalidStudentIdException()
        val schoolId = it["schoolId"]?.toString()?.toLongOrNull() ?: throw InvalidSchoolIdException()

        val school = schoolRepository.findSchoolBySchoolId(schoolId)

        if(school!=null){
            Student(studentId, schoolId, school)
        } else {
            null
        }

    }

    class InvalidStudentIdException : RuntimeException()
    class InvalidSchoolIdException : RuntimeException()

}
And added the resolver to the federated type registry in the SpringBootApplication class:
Copy code
@SpringBootApplication
class DemoApplication

@Bean
fun federatedTypeRegistry(studentResolver: StudentResolver): FederatedTypeRegistry =
    FederatedTypeRegistry(mapOf("Student" to studentResolver))

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}
However the problem is still there. I've put a breakpoint inside the resolver but it never triggers. I've also made sure that the package for both the student and the resolver is declared in our application.yml
Not sure what else is missing.
I've also enabled debugging on the gateway and path traversal seems correct to me:
Copy code
QueryPlan {
  Sequence {
    Fetch(service: "vcdb") {
      {
        getByStudentId(studentId: 1) {
          schoolId
          studentId
          studentName
          __typename
        }
      }
    },
    Flatten(path: "getByStudentId") {
      Fetch(service: "pcdb") {
        {
          ... on Student {
            __typename
            studentId
          }
        } =>
        {
          ... on Student {
            school {
              schoolName
            }
          }
        }
      },
    },
  },
}
d
shouldn’t your configuration be
Copy code
@SpringBootApplication
class DemoApplication {
  @Bean
  fun federatedTypeRegistry(studentResolver: StudentResolver): FederatedTypeRegistry =
    FederatedTypeRegistry(mapOf("Student" to studentResolver))
}
i.e.
@Bean
definitions should be included inside configuration class and not top level functions
h
Gosh I can't believe I missed the braces' position! Yes it seems to be properly wired now. The breakpoints are working now. Thank you so much!
d
👍