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)
})
}
Related
I'm setting up a NestJS service using GraphQL to be the middleman between the UI and multiple other services. For the sake of this example, I need to display a list of books with their relevant information from a single publisher.
The UI will hit the nest service with an id of a publisher. I need to then call a service and get a list of book Ids. When I have the list of Ids I need to hit two separate services, each return a list of objects for each book id. I then need to build a list of book objects from both datasets.
An example of the book models is:
export class BookModel {
#Field(type => BookInformationModel)
bookInfo: BookInformationModel;
#Field(type => BookSalesModel)
bookSales: BookSalesModel;
}
An example of the flow is:
UI hits Nest service with a publisher Id "pub1"
Nest service goes to the publisher-service which returns a list of books linked to the publisher ['book1', 'book2']
Nest service then hits the book-info-service that returns [{id: 'book1' title: ...}, {id: 'book2' title: ...}]
Nest service then hits the book-sales-service that returns [{price: 123 currency: ...}, {price: 456 currency: ...}]
Map both data sets to a list of BookModel
[{
bookInfo: {id: 'book1' title: ...}
bookSales: {price: 123 currency: ...}
}, {
bookInfo: {id: 'book2' title: ...}
bookSales: {price: 456 currency: ...}
}]
This is a simplified version of the resolver where I'm having trouble:
#Query(returns => [BookModel])
async getBooksByPublisherId(
#Args('id', { type: () => String }) id: string
) {
this.publisherService.getBookIds(id)
.then(response => {
return response.data;
})
}
#ResolveField('bookInfo', returns => BookInformationModel)
async getBookInfo() {
return this.bookInfoService.getBookInfo(bookIds)
}
#ResolveField('bookSales', returns => BookSalesModel)
async getBookSalesInfo() {
return this.bookSalesService.getSalesInfo(bookIds)
}
There are two issues I'm having:
How do I share the list of book Ids with the field resolvers?
I'm not sure how I write the field resolvers for bookInfo, bookSales as the services return a list of objects.
Figured it out. I needed to have the bookId on the BookModel. That way the field resolvers can access it via #Parent.
export class BookModel {
#Field()
bookId: string;
#Field(type => BookInformationModel)
bookInfo: BookInformationModel;
}
#Query(returns => [BookModel])
async getBooksByPublisherId(
#Args('id', { type: () => String }) id: string
) {
this.publisherService.getBookIds(id)
.then(response => {
return response.data.map(bookID => ({ bookId }));
})
}
#ResolveField('bookInfo', returns => BookInformationModel)
async getBookInfo(
#Parent() bookModel: BookModel
) {
return this.bookInfoService.getBookInfo(bookModel.bookId)
}
It seems reasonable to expect one resolver to handle input for any combination of one or more of an object's values. I shouldn't have to write separate resolvers for 'title', 'published', 'author', etc., right?
Here's my example object:
let books = [
{
title: 'Clean Code',
published: 2008,
author: 'Robert Martin',
id: 'afa5b6f4-344d-11e9-a414-719c6709cf8e',
genres: ['refactoring'],
},
{
title: 'Agile software development',
published: 2002,
author: 'Robert Martin',
id: 'afa5b6f5-344d-11e9-a414-719c6709cf9e',
genres: ['agile', 'patterns', 'design'],
},
]
typeDefs:
const typeDefs = gql`
type Book {
title: String
published: Int
author: String
id: ID
genres: [String]
}
type Query {
bookCount: Int!
allBooks(title: String, author: String, genre: String): [Book]
findBooks(title: String!): Book
}
type Mutation {
addBook(
title: String!
published: Int
author: String!
genres: [String]
): Book
editBook(
id: ID
title: String
published: Int
author: String
genres: [String]
): Book
}
`
Here's the resolver I currently have:
Mutation: {
editBook: (_, args) => {
const book = books.find(b => b.id === args.id)
if (!book) {
return null
}
const updatedBook = {
...book,
title: args.title,
author: args.author,
published: args.published,
genres: [args.genres],
}
books = books.map(b => (
b.id === args.id ? updatedBook : b))
return updatedBook
},
}
Here's what is currently happening.
Original object:
"allBooks": [
{
"id": "afa5b6f4-344d-11e9-a414-719c6709cf8e",
"title": "Clean Code",
"author": "Robert Martin",
"published": 2008,
"genres": [
"refactoring"
]
},
{...}
]
Mutation query:
mutation {
editBook(id:"afa5b6f4-344d-11e9-a414-719c6709cf8e", title:"changed"){
id
title
author
published
genres
}
}
Returns this:
{
"data": {
"editBook": {
"id": "afa5b6f4-344d-11e9-a414-719c6709cf8e",
"title": "changed",
"author": null,
"published": null,
"genres": [
null
]
}
}
}
How do I write the resolver to change one or more of an object's values, without changing the unspecified values to 'null'?
My javascript skills are, I'll admit, rather shaky, and I'm guessing the answer lies with a more eloquent map function, but since the code runs inside a graphql schema module, it doesn't handle console.log so troubleshooting is problematic. Any recommendations to address that would be extremely helpful as well, so I can troubleshoot my own problems better.
Couple of points
There's no need to use map or to reassign books. Because the book variable is a reference to the object in your books array, you can mutate it (but not reassign it) and you will mutate the object in the array. See this question for additional details.
const book = books.find(b => b.id === args.id)
if (book) {
// you cannot reassign book and still have the original value change,
// but you can mutate it
book.title = args.title
book.author = args.author
book.published = args.published
book.genres = args.genres
}
return book
To change only the values present in args, you have some options:
You can use the logical OR operator (||) to provide another value to assign if the first value is falsey. Here, book.title will only be used if args.title is undefined, null, 0, NaN, "", or false.
book.title = args.title || book.title
You can use Object.assign, which copies the properties from one object to another. Since args will be missing those properties that aren't provided, you can just do something like the below snippet. This is much more elegant and concise, but will only work if your argument names match your property names.
Object.assign(book, args)
You can loop through the args object. This might be more desirable if you have one or more arguments that don't match the properties of book:
Object.keys(args).forEach(argName => {
const argValue = args[argName]
book[argName] = argValue
})
Putting it all together, we might do:
const book = books.find(b => b.id === args.id)
if (book) {
Object.assign(book, args)
}
return book
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
I have two collections:
dbPosts
id: mongoose.Schema.Types.ObjectId,
title: { type: String },
content: { type: String },
excerpt: { type: String },
slug: { type: String },
author: {
id: { type: String },
fname: { type: String },
lname: { type: String },
}
dbAuthors
id: mongoose.Schema.Types.ObjectId,
fname: { type: String },
lname: { type: String },
posts: [
id: { type: String },
title: { type: String }
]
I resolve my author queries as follows:
Query: {
authors: (parent, root, args, context) => {
return dbAuthor.find({});
},
author: (root, args, context) => {
return dbAuthor.findById(args.id);
},
},
Author: {
posts: (parent) => {
if(parent.posts) {
return parent.posts;
} else {
return dbAuthor.find({'author.id': parent.id});
}
},
}
The reason I'm resolving thus is to optimize my MongoDB requests by denormalizing my relationships. Here's the objective:
If you need just a list of authors with the titles of their works, all necessary fields are right there in dbAuthors, so no need to look up dbPosts. But if you need more details on each post returned, say, excerpts or slug, you look up dbPosts for the following condition:
{'author.id': parent.id}
But the problem is, if your query looks like this:
authors(id: "3") {
fname
lname
posts {
title
excerpt
}
}
it breaks, because there's no excerpt field returned in the parent object. This problem could be easily fixed if there were some way I could determine what fields are being queried on an author's posts field and then decide if the values returned in parent would suffice. If not, I could then proceed to look up dbPosts with the author's id value. Is it possible? Because if not, it would defeat the whole purpose of denormalizing your collections, something Mongo strongly urges you to do!
It's rather denormalized - data is duplicated ;)
You're probably looking for info.fieldNodes
I have a postgres table that represents a hierarchy with a parent child table:
Table (Categories):
id name parentId
1 CatA null
2 CatB null
3 CatC 1
4 CatD 1
5 CatE 3
desired result:
categories:
[
{
name: "CatA",
children: [
{
name: "CatC",
children: [
{
name: "CatE",
children: []
}]
},
{
name: "CatD",
children: []
}
],
},
{
name: "CatB",
children: []
}
]
The problem is that I don't know how many levels there are, so I can't query something like:
category {
name
parent {
name
parent {
name
...
You can actually achieve the potential infinite recursion with GraphQL. So it doesn't mind if you don't know how deep you go with your schema.
I was able to reproduce your desired result with this schema. I hope it might helps you:
const categories = [
{
name: 'CatA',
children: [
{
name: 'CatC',
children: [
{
name: 'CatE',
children: []
}]
},
{
name: 'CatD',
children: []
}
]
},
{
name: 'CatB',
children: []
}
];
const categoryType = new GraphQLObjectType({
name: 'CategoryType',
fields: () => ({
name: { type: GraphQLString },
children: { type: new GraphQLList(categoryType) }
})
});
const queryType = new GraphQLObjectType({
name: 'RootQuery',
fields: () => ({
categories: {
type: new GraphQLList(categoryType),
resolve: () => categories
}
})
});
And I got this result:
Please notice that I define field property as a function rather than an plain object. The field property defined as object would failed and wouldn't allow you to use categoryType variable in the fields, because in the time of execution it doesn't exist.
fields: () => ({
...
})
One of the difficulties of using GraphQL on top of a SQL database is reconciling the two paradigms. GraphQL is hierarchical. SQL databases are relational. There isn't always a clear mapping between the two.
We open-sourced a framework, Join Monster, that has an opinionated way of setting up your schemas. If you do so, it automatically generates the SQL queries for you. It was built with the idea of relations in its core. In theory you can achieve arbitrary depth in your GraphQL queries.