How do I query nested documents related by ObjectID with GraphQL - node.js

USER has a field 'groups' which is an array of objectID's corresponding to GROUP
GROUP has a field 'boards' which is an array of objectID's corresponding to BOARD
BOARD has a field 'posts' which is an array of objectID's corresponding to POST
POST has a field 'comment' which is an array of objectID's corresponding to COMMENT
My goal here is to be able to query GrapQL like so:
Query{
user('_id'){
username
groups{
name
boards{
name
posts{
comment{
text
}
}
}
}
}
}
So by only supplying the query with the user's ID, I can get all the groups, boards, posts, and comments relative to the user.
My Type Defs are as follows:
type Query {
user(_id: String): User
groups(_id: String): [Group]
boards(_id: String): [Board]
posts(_id: String): [Post]
comments(_id: String): [Comment]
}
type User {
_id: String,
username: String,
email: String,
Groups: [Group]
}
type Group {
_id: String,
name: String,
bio: String,
owner: User
members: [User]
boards: [Board]
}
type Board {
_id: String,
name: String,
bio: String,
group: Group
posts: [Post]
}
type Post {
_id: String,
name: String,
board: Board,
comments: [Comment]
}
type Comment {
_id: String
text: String
}
schema {
query: Query
}
My resolvers (where I'm sure the problem lies):
const resolvers = {
Query: {
user: async (root, { _id }) => {
return prepare(await Users.findOne(ObjectId(_id)))
},
groups: async (root, { _id }) => {
return prepare(await Groups.findOne(ObjectId(_id)))
},
boards: async (root, { _id }) => {
return prepare(await Boards.findOne(ObjectId(_id)))
},
posts: async (root, { _id }) => {
return prepare(await Posts.findOne(ObjectId(_id)))
},
comments: async (root, { _id }) => {
return prepare(await Coments.findOne(ObjectId(_id)))
}
},
User: {
groups: async ({_id}) => {
return (await Groups.find({_id: _id}).toArray()).map(prepare)
}
}
}
Basically I'm trying to makle a big, beefy, nested query with a single call. All the models are related by objectID's. Does anyone have any input on how to proceed here or perhaps some links to related documentation? I've searched all over, but can't seem to find docs related to this specifically. Querying related documents by objectID.

Related

GraphQL/ Apollo Server: Can't Access nested object

Problem: Trying to query for products in user's basket breaks with the following error:
"ID cannot represent value: <Buffer 60 41 24 0c ae a8 b6 35 ac 33 5a cd>"
my query:
query getBasket{
getBasket {
id
quantity
createdAt
product{
id <--- produces error
title <--- removing ID, product.title becomes null
stock <--- becomes null if ID is removed from query
}
}
}
If I omit the product field, everything works as expected but I need to be able to display the product information for the user's basket. When I try retrieve the product information I get the error.
My GraphQL definitions are the following (I am new to this), there should be a one to many relationship between User and Item (which represents the items in their basket):
module.exports = gql(`
enum Role {
ADMIN
USER
}
type Item {
id: ID!
product: Product!
quantity: Int!
createdAt: String!
}
type User {
id: ID!
email: String!
token: String!
roles: [Role]!
basket: [Item]!
createdAt: String!
}
type Product {
id: ID!
title: String!
description: String!
stock: Boolean
price: String
}
input RegisterInput {
email: String!
password: String!
confirmPassword: String!
}
type Query {
getProducts: [Product]
getProduct(productId: ID!): Product
getBasket: [Item]
getUsers: [User]
}
type Mutation {
register(registerInput: RegisterInput): User!
login(email: String!, password: String!): User!
createProduct(title: String!, description: String, stock: Boolean, price: String!): Product!
deleteProduct(productId: ID!): String!
addToBasket(productId: String!, quantity: String!): User!
deleteFromBasket(productId: String!): User!
}
`);
I am able to add products into the basket, it's just when I try to retrieve the user's basket I get the error.
Click this to see how my data looks like on MongoDB atlas: https://i.stack.imgur.com/RKcnP.png
There's only a couple of posts about the error I tried converting the string ID to object ID. It could be a problem with GraphQL? I'm unsure, perhaps I need to redo my GraphQL Definitions.
User Schema:
const userSchema = new Schema({
email: String,
password: String,
roles: [String],
basket: [
{
product: {
type: Schema.Types.ObjectId,
ref: 'products'
},
quantity: String,
createdAt: String
}
],
createdAt: String
});
AddToBasket Mutation:
Mutation: {
addToBasket: async (_, {productId, quantity}, context) => {
// TODO: Validate input fields
console.log("adding to basket")
const userContext = checkAuth(context, USER);
const user = await User.findById(userContext.id);
const product = await Product.findById(productId);
if (product) {
user.basket.unshift({ //this adds the following into the basket as an object
product: product.id,
quantity,
createdAt: new Date().toISOString()
});
await user.save();
return user;
} else throw new UserInputError('Product not found');
}
}
Thank you for helping!
Edit 11:51 06/03/2021: (removed)
Edit 2 12:25 06/03/2021:
GraphQLError: ID cannot represent value: <Buffer...>"
My problem is the exact same problem as this persons but I think their code is different to mine? How I return the user's basket is by the following:
Query: {
getBasket: async (_, {}, context) => {
console.log("RUNNING");
const userContext = checkAuth(context, USER);
const user = await User.findById(userContext.id);
return user.basket;
}
},
Edit 3 12:52 06/03/2021:
Here is my user Schema:
const { model, Schema } = require('mongoose');
const userSchema = new Schema({
email: String,
password: String,
roles: [String],
basket: [
{
product: {
type: Schema.Types.ObjectId,
ref: 'products'
},
quantity: String,
createdAt: String
}
],
createdAt: String
});
module.exports = model('User', userSchema);
my schema references were wrong, I figured that out after reading one of the answers for this post: MissingSchemaError: Schema hasn't been registered for model "User"
basket: [
{
product: {
type: Schema.Types.ObjectId,
ref: 'products' <--- changed to 'Product' (the name of my 'Product'Schema)
},
quantity: String,
createdAt: String
}
],
I figured out that my nested objects weren't populated, so I read this for better understanding: https://dev.to/paras594/how-to-use-populate-in-mongoose-node-js-mo0 . I figured this out by console.logging the user.basket value (and other fields) before returning user.basket, I found that the user.basket was undefined.
I redid my getBasket Query to the following, here I'm ensuring I'm populating the basket field and the product field inside the basket field.
Without .populate, the error "ID cannot represent value: <Buffer..." would prop up again.
Query: {
getBasket: async (_, {}, context) => {
console.log("RUNNING");
const userContext = checkAuth(context, USER);
const user = await User.findById(userContext.id, {basket:1})
.populate({
path: "basket", // populate basket
populate: {
path: "product", // in basket, populate products
select: {title:1, stock:1, price:1}
}
})
.exec();
return user.basket;
}
},
If your query result is null, then read this answer for better understanding: Why does a GraphQL query return null?.
ANSWER = POPULATE THE NESTED FIELD
Feel free to post any improvements or changes that could be made.
Thank you Ezra for helping me!

GraphQL and Mongoose: When querying, populate references in subdocuments

This is my schema, and in MongoDB I have separate collections for users, events and movies due to the large number of events and movies:
type Event {
id: ID!
title: String!
description: String!
creator: User!
}
type Movie {
id: ID!
title: String!
releaseDate: String
}
type MovieStatus {
movie: Movie
status: StatusEnum // WATCHED, PLANNING_TO_WATCH
}
type User {
id: ID!
firstName: String!
createdEvents: [Event!]
movies: [MovieStatus!]
}
And I want to create the following query:
query {
user(id: "12345") {
id
firstName
createdEvents{
id
title
description
}
movies{
movie {
id
title
releaseDate
}
status
}
}
}
I have no problem getting the events, by including this in my resolvers:
User: {
createdEvents: ({ createdEvents }) =>
Event.find({ _id: { $in: createdEvents } }),
},
But I can't figure out how to access the movie id, title and release dates.
movies: ({ movies }) => {
return Promise.all(
movies.map(async (m) => {
const movie = m
movie.movie = await Movie.findById(movie.movie)
return movie
})
)
}

Nested Mongoose queries data not showing on GraphQL query

I'm trying to create an API using Apollo-server with GraphQL and Mongoose.
My problem is that the query to Mongoose does return the data, but the GraphQL model shows as null when I test.
I've tried different methods and using promises.
type Articulo {
id: ID
nombre: String
codigoDeBarras: String
codigo: String
}
type EntradaInventario {
id: ID
articulo: Articulo
inventario: Inventario
cantidad: Float
}
type Almacen {
id: ID
nombre: String
}
type Inventario {
id: ID
almacen: Almacen
nombre: String
}
type Query {
articulo(codigoDeBarras: String): Articulo
entradaInventario(inventario: String): [EntradaInventario]
}
type Mutation {
addEntradaInventario(idArticulo: String, idInventario: String, cantidad: Float): EntradaInventario
addAlmacen(nombre: String): Almacen
addInventario(idAlmacen: String, nombre: String): Inventario
}
const EntradaInventarioModel = Mongoose.model("EntradaInventario", {
_id: Schema.Types.ObjectId,
idArticulo: {type: Mongoose.Schema.Types.ObjectId, ref: 'Articulo'},
idInventario: {type: Mongoose.Schema.Types.ObjectId, ref: 'Inventario'},
cantidad: Number
}, "entradainventarios");
Query: {
articulo: (_, args) => ArticuloModel.findOne({
'codigoDeBarras': args.codigoDeBarras
}).exec(),
entradaInventario: (_, args) =>
EntradaInventarioModel.find({idInventario: args.inventario})
.populate('idArticulo')
.exec(),
}
You shouldn't use the populate on the Parent model. You should instead define how to query the nested model in the EntradaInventario resolvers.
Inventory should look something like this:
Inventory : {
articulo: async (parent, args, { }) => {
return await Articulo.findOne({
_id: parent.idArticulo,
});
},
}
Here is a repo that does just that and is a good example https://github.com/the-road-to-graphql/fullstack-apollo-express-mongodb-boilerplate

graphql.js: field is null even though resolver is returning value for sequilize query

I have the following graphql schema and I am trying to execute businessUser query
type Query {
businessUser(id: String!): BusinessUser
}
type BusinessUser {
id: ID!,
firstName: String,
lastName: String,
email: String,
posts: [BusinessPostFormat]
}
type BusinessPostFormat {
postId: Int,
user: String,
isActive: Boolean,
postText: String
}
In my resolver I am joining my Post table with my businessPost table to retrieve a 'isActive' column. When I print out what Sequilize is returning I see that I am getting back exactly the data object that should map to the BusinessPostFormat type:
post {
dataValues:
{ postId: '188',
postText: 'blah blah blah',
user: 'userName',
isActive: false },
However when I execute the query in graphiql the isActive field is coming back as null. I dont understand why thats happening if I can see that the resolver is returning exactly what BusinessPostFormat type is expecting?
resolver function below:
const resolvers = {
Query: {
businessUser(_, args) {
return BusinessUser.findById(args.id);
}
},
BusinessUser: {
posts(businessUser) {
return businessUser.getPosts({
attributes: {
include: [[Sequelize.literal('business_posts.is_active'), 'isActive']],
exclude: ['business_user_id']
},
include: [{ model: BusinessPost, attributes: ['is_active'] }]
})
},
}
Maybe you are missing the fact that Sequelize uses instances http://docs.sequelizejs.com/manual/tutorial/instances.html and maybe that is the missing fact in this case.
Try it like this.
return Entries.findAll().then(entries => {
return entries.map(e => e.get({ plain: true }));
})

Updating a double nested array MongoDB

Consider this schema:
let userSchema = new mongoose.Schema({
id: String,
displayName: String,
displayImage: String,
posts: [
{
url: String,
description: String,
likes: [String],
comments: [
{ content: String, date: String, author: { id: String, displayName: String, displayImage: String } }
]
}
]
});
I am able to delete a certain item from the comments array using this query
controller.deleteComment = (req, res, next) => {
User.findOneAndUpdate(
{ id: req.query.userid, 'posts._id': req.params.postid, },
{
$pull: {
'posts.$.comments': { _id: req.body.commentID },
}
}
)
.exec()
.then(() => {
res.send('deleted');
})
.catch(next);
};
Is there anyway that I can UPDATE an element inside the comments array by using the $set operator? I need to changed the contents of a comment based on its comment ID.. something like this:
controller.editComment = (req, res, next) => {
User.findOneAndUpdate(
{ id: req.query.userid, 'posts._id': req.params.postid, 'comments._id':req.body.commentID },
{
$set: {
'posts.$.comments': { content: req.body.edited },
}
}
)
.exec()
.then(() => {
res.send('deleted');
})
.catch(next);
};
This ^ is obviously not working but I'm wondering if there is a way I can do this?
UPDATE
As per suggestions below I am doing the following to manage only one Schema. This works, however only the first post's comments get updated regardless of which posts comments I am editing. I have checked and the return docs are always correct. There must be a problems with the doc.save() method.
controller.editComment = (req, res, next) => {
User.findOne(
{ id: req.query.userid, 'posts._id': req.params.postid },
{ 'posts.$.comments._id': req.body.commentID }
)
.exec()
.then((doc) => {
let thisComment = doc.posts[0].comments.filter((comment) => { return comment._id == req.body.commentID; });
thisComment[0].content = req.body.edited;
doc.save((err) => { if (err) throw err; });
res.send('edited');
})
.catch(next);
};
I don't know a simple (or even tough :P) way to achieve what are trying to do. In mongo, manipulation is comparatively tough in doubly nested arrays and hence, best to be avoided.
If you are still open for schema changes, I would suggest you create a different schema for comments and refer that schema inside user schema.
So your comment schema will look like this:
let commentSchema = new mongoose.Schema({
content: String,
date: String,
author: {
id: String,
displayName: String,
displayImage: String
}
});
And you user schema should look like this:
let userSchema = new mongoose.Schema({
id: String,
displayName: String,
displayImage: String,
posts: [{
url: String,
description: String,
likes: [String],
comments: [{
type: Schema.Types.ObjectId,
ref: 'comment' //reference to comment schema
}]
}]
});
This way your data manipulation will be a lot easier. You can populate comments while fetching user document. And, notice how easy update/delete operations are, given that you already know that _id of the comments you want to update.
Hope you find this answer helpful!
controller.editComment = (req, res, next) => {
User.findOneAndUpdate(
{ id: req.query.userid, 'posts._id': req.params.postid, 'comments._id':req.body.commentID },
{
$push: {
'posts.$.comments': { content: req.body.edited },
}
}
)
.exec()
.then(() => {
res.send('deleted');
})
.catch(next);
};

Resources