Merging Rest datasets in Federation with resolvers? - graphql

pretty new to GraphQL and Apollo Federation.
I have a question, is it possible to populate one dataset with another such as:
# in Shop Service
type carId {
id: Int
}
type Shop #key(fields: "id") {
id: ID!
name: String
carIds: [CarId]
}
# in Car Service
type Car {
id: ID!
name: String
}
extends type Shop #key(fields: "id") {
id: ID! #external
cars: [Car]
}
Car Resolver
Query{...},
Shop: {
async cars(shop, _, { dataSources }) {
console.log(shop); // Issue here is it returns the references that are an object only holding the `id` key of the shop, I need the `cars` key here, to pass to my CarsAPI
return await dataSources.CarsAPI.getCarsByIds(shop.carsIds);
}
}
From the Shop rest api the response would look like:
[{id: 1, name: "Brians Shop", cars: [1, 2, 3]}, {id: 2, name: "Ada's shop", cars: [4,5,6]}]
From the Car rest api the response would look like:
[{id: 1, name: "Mustang"}, {id: 2, name: "Viper"}, {id: 3, name: "Boaty"}]
So what I want to archive is to query my GraphQL server for:
Shop(id: 1) {
id
name
cars {
name
}
}
And then expect:
{
id: 1,
name: "Brian's shop",
cars: [
{name: "Mustang"},
{name: "Viper"},
{name: "Boaty"}
]
}
Is this possible, it was what I thought when I chose federation :)

So, if I understand correctly after your comments, what you want is to have the carIds from Shop service coming in your Car service inside the cars resolver.
You can make use of the #requires directive which will instruct Apollo Server that you need a field (or a couple of) before it starts executing the cars resolver. That is:
Car Service
extend type Shop #key(fields: "id") {
id: ID! #external
carIds: [Int] #external
cars: [Car] #requires(fields: "carIds")
}
Now, inside the cars resolver, you should be able to access shop.carIds on your first parameter.
See: https://www.apollographql.com/docs/apollo-server/federation/advanced-features/#computed-fields

Related

How can I provide input field arguments with apollo federation 2?

I am trying to federate two of my micro services with apollo gql federation 2.
I have successfully connected the two services through the federation with the following schemas:
Subgraph1 - Product
type Product #key(fields: "id") {
id: ID!
title: String!
description: String
price: Int!
category: [Category!]
}
type Category #key(fields: "id") {
id: ID!
}
type Query {
product(id: ID!): Product
}
Subgraph 2 - Category
type Category #key(fields: "id") {
id: ID!
title: String
}
and the following query
query Product($productId: ID!) {
product(id: $productId) {
id
title
category {
id
title
}
}
}
gives a desired result
However, what if I wanted to add some filter on the returned categories for a given product. Lets say I only wanted to have the ones with title "sport", so the query would look like this instead:
query Product($productId: ID!) {
product(id: $productId) {
id
title
category(searchTerm: "sport") {
id
title
}
}
}
A normal way of doing the input argument would be simply just
type Product #key(fields: "id") {
id: ID!
title: String!
description: String
price: Int!
category(searchTerm: String): [Category!]
}
Is this achievable when federating the services? I am not sure how the input field is provided to the second subgraph?
I tried to add the input as a part of the type in the first subgraph, however it does not seem to pass the search term to the next graph.

Spring GraphQLmultiple schemas with Query per file

with Spring-GraphQl if I have following two schemas in the resources/graphql folder:
schema1:
type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
schema2:
type Query {
personByName(name: String): Person
}
type Person {
id: ID
firstName: String
lastName: String
}
Spring-GraphQL seems to be merging them into one GraphQL schema file and starting of Spring-Boot Graphql app ends with following error:
Caused by: graphql.schema.idl.errors.SchemaProblem: errors=['Query' type [#1:1] tried to redefine existing 'Query' type [#1:1]]
When I change it to:
schema1:
type Query {
bookById(id: ID): Book
personByName(name: String): Person
}
schema2:
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
type Person {
id: ID
firstName: String
lastName: String
}
it works perfectly good and I am able to call both queries with graphiql. How graphql spring works with multiple schemas? It seems spring-graphql merges files into one schema so multiple Query types per file breaks the app.
Thanks for answer.
Spring GraphQL is loading all schema resources under the configured location and is using TypeDefinitionRegistry::merge to create a single schema out of them.
I think that redifining any type (even the Query one) should raise an error, otherwise this could hide important issues and conflicting schema definitions. That's what GraphQL Java's TypeDefinitionRegistry is doing.
You can organize your schema files like this:
graphql/schema.graphqls
type Query {
}
// add common directives, scalars, etc
graphql/books.graphqls
extend type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
graphql/person.graphqls
extend type Query {
personByName(name: String): Person
}
type Person {
id: ID
firstName: String
lastName: String
}

Apollo federation: Field/resolver that returns entities from two separate services that implement an interface

Summary
In GraphQL Apollo managed federation, I'm trying to figure out if it's possible for one service to have an aggregate field, that returns an array of entities two separate services. As in, I'm trying to support this use case:
query FromTwoServices {
aggregateField {
id
...on Service1Entity {
field1
}
...on Service2Entity {
field2
}
}
}
where both
type Service1Entity implements Aggregation
and
type Service2Entity implements Aggregation
And under the hood, the resolver for aggregateField (which lives on Service 2) calls down down to Service 1 to Service1Entitys. So this field is aggregating two data sets.
Example Problem
In my current GraphQL setup, I have all of this defined in one service:
interface Animal {
id: ID!
name: String!
}
type Cat implements Animal {
id: ID!
name: String!
meows: Boolean
}
type Dog implements Animal {
id: ID!
name: String!
barks: Boolean
}
When I query my service, it hits an animals resolver, and looks like:
query Animals {
animals {
id
...on Cat {
meows
}
...on Dog {
barks
}
}
}
Now I'm creating a Dogs service. I would like Dogs to come from the Dog service, and Cats to come from the Cats service. Additionally, I would still like to support my animals aggregation field, and I'd like to move it to the Dogs service.
To support this, I've made Cat an entity, and duplicated the interface across both services. To allow Dog service to resolve the Cat type, I've used "referencing" to tell Dog service that Cat lives somewhere else.
Cat Service (marking Cat as entity with #key):
interface Animal {
id: ID!
name: String!
}
type Cat implements Animal #key(fields: "id") {
id: ID!
name: String!
meows: Boolean
}
Dog Service:
interface Animal {
id: ID!
name: String!
}
type Cat implements Animal #key(fields: "id") {
id: ID! #external
}
type Dog implements Animal {
id: ID!
name: String!
barks: Boolean
}
The problem with this setup is the Dog service fails to start, because of this GraphQL error:
Error: Interface field Animal.name expected but Cat does not provide it.
In my Dog service, if I add an external name field, like so:
type Cat implements Animal #key(fields: "id") {
id: ID! #external
name: String! #external
}
Then the two services fail to compose in managed federation:
This data graph is missing a valid configuration. [dog] Cat.name -> is marked as #external but is not used by a #requires, #key, or #provides directive.
I sort of feel like I'm going down the wrong path here. Is it possible to have an aggregate field like this, where one service can return the its own entities, combined with the entities of another service?
We already have lots of clients querying for the animals field, so we're trying to support this change without requiring client changes, aka without restructuring the use of an interface.

GraphQL - Relationship returning null

I started to learn GraphQL and I'm trying to create the following relationship:
type User {
id: ID!,
name: String!,
favoriteFoods: [Food]
}
type Food {
id: ID!
name: String!
recipe: String
}
So basically, a user can have many favorite foods, and a food can be the favorite of many users. I'm using graphql.js, here's my code:
const Person = new GraphQLObjectType({
name: 'Person',
description: 'Represents a Person type',
fields: () => ({
id: {type: GraphQLNonNull(GraphQLID)},
name: {type: GraphQLNonNull(GraphQLString)},
favoriteFoods: {type: GraphQLList(Food)},
})
})
const Food = new GraphQLObjectType({
name: 'Food',
description: 'Favorite food(s) of a person',
fields: () => ({
id: {type: GraphQLNonNull(GraphQLID)},
name: {type: GraphQLNonNull(GraphQLString)},
recipe: {type: GraphQLString}
})
})
And here's the food data:
let foodData = [
{id: 1, name: 'Lasagna', recipe: 'Do this then that then put it in the oven'},
{id: 2, name: 'Pancakes', recipe: 'If you stop to think about, it\'s just a thin, tasteless cake.'},
{id: 3, name: 'Cereal', recipe: 'The universal "I\'m not in the mood to cook." recipe.'},
{id: 4, name: 'Hashbrowns', recipe: 'Just a potato and an oil and you\'re all set.'}
]
Since I'm just trying things out yet, my resolver basically just returns a user that is created inside the resolver itself. My thought process was: put the food IDs in a GraphQLList, then get the data from foodData usind lodash function find(), and replace the values in person.favoriteFoods with the data found.
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
description: 'Root Query',
fields: {
person: {
type: Person,
resolve(parent) {
let person = {
name: 'Daniel',
favoriteFoods: [1, 2, 3]
}
foodIds = person.favoriteFoods
for (var i = 0; i < foodIds.length; i++) {
person.favoriteFoods.push(_.find(foodData, {id: foodIds[i]}))
person.favoriteFoods.shift()
}
return person
}
}
}
})
But the last food is returning null. Here's the result of a query:
query {
person {
name
favoriteFoods {
name
recipe
}
}
}
# Returns
{
"data": {
"person": {
"name": "Daniel",
"favoriteFoods": [
{
"name": "Lasagna",
"recipe": "Do this then that then put it in the oven"
},
{
"name": "Pancakes",
"recipe": "If you stop to think about, it's just a thin, tasteless cake."
},
null
]
}
}
}
Is it even possible to return the data from the Food type by using only its ID? Or should I make another query just for that? In my head the relationship makes sense, I don't think I need to store the IDs of all the users that like a certain food in the foodData since it has an ID that I can use to fetch the data, so I can't see the problem with the code or its structure.
Calling shift and push on an array while iterating through that same array will invariably lead to some unexpected results. You could make a copy of the array, but it'd be much easier to just use map:
const person = {
name: 'Daniel',
favoriteFoods: [1, 2, 3],
}
person.favoriteFoods = person.favoriteFoods.map(id => {
return foodData.find(food => food.id === id)
})
return person
The other issue here is that if your schema returns a Person in another resolver, you'll have to duplicate this logic in that resolver too. What you really should do is just return the person with favoriteFoods: [1, 2, 3]. Then write a separate resolver for the favoriteFoods field:
resolve(person) {
return person.favoriteFoods.map(id => {
return foodData.find(food => food.id === id)
})
}

Graphql with mutation spring boot

My schema file is
type Mutation {
createCustomer(name: String!, email: String!, product: [Product]): Customer
}
input Product {
id: ID!
name: String!
price: Int
}
interface Person {
id: ID!
name: String!
email: String!
}
type Customer implements Person {
id: ID!
name: String!
email: String!
product: [Product]
}
I want to insert customer detail here which has product list as input. My query is
mutation {
createCustomer(
name: "kitte",
email: "kitte#gmail.com",
product: [
{
name: "soap",
price: 435,
}
]
)
{
id
name
email
product{name}
}
}
But I am getting exception
{
"data": null,
"errors": [
{
"validationErrorType": "WrongType",
"message": "Validation error of type WrongType: argument value ArrayValue{values=[ObjectValue{objectFields=[ObjectField{name='name', value=StringValue{value='dars76788hi'}}, ObjectField{name='price', value=IntValue{value=123}}]}, ObjectValue{objectFields=[ObjectField{name='name', value=StringValue{value='darr'}}, ObjectField{name='price', value=IntValue{value=145}}]}]} has wrong type",
"locations": [
{
"line": 5,
"column": 5
}
],
"errorType": "ValidationError"
}
]
}
I don't understand what is the error. And how to pass list to mutation. I have referred some examples but not able to insert product as list.
Make sure you are passing the right type of objects to your mutation. GraphQL needs separate types for input fields. In your schema, Product types should be something like this and you should change the mutation accordingly.
type Product {
id: ID!
name: String!
price: Int
}
input ProductInput {
name: String!
price: Int
}
input CustomerInput {
...
products: [ProductInput]
}
There are couple of very useful examples in the docs, see Mutations and Input Types

Resources