I am currently working on a project with mongoose, and I have been stuck in a problem with the mongoose populate() method.
The problem is that I cannot populate an object inside an object in an array.
// * I simplified the code below.
// Profile Document
Profile = model('Profile', new Schema({
_user: {
type: Schema.Types.ObjectId,
ref: 'User',
},
posts: [
{
type: Schema.Types.ObjectId,
ref: 'Post',
// Post = {
// _id: Number,
// _poster: {
// type: Schema.Types.ObjectId,
// ref: 'User',
// }
// };
},
],
}));
// app.js
const profile = await Profile.findOne({ _user: userId })
.populate('_user') // it works
.populate('posts') // it works too
.populate('posts._poster'); // it doesn't work
Is there any way to populate a nested object in an array?
It'd be great if you could answer my question. Thank you in advance.
I tried accessing the notation properly to populate *_poster*, however, it didn't still work. The code is below.
const profile = await Profile.findOne({ _user: userId })
.populate('_user')
.populate('posts');
await profile.toObject();
profile.posts.forEach((post) => {
profile.populate('posts.post._poster');
})
I have tested your schema this works great, could you test and let me know
const profile = await Profile.findOne({
_id: '5f108ed7ecc4881a6c35e27b',
})
.populate('_user')
.populate({
path: 'posts',
populate: {
path: '_poster',
model: 'User',
},
})
.exec();
console.log(profile);
Related
I have been on this for days. I have tried so many things. I know I am not doing it rightly. I have a blog application I am building using node.js, mongodb and react.js. So, I have User Model, Post Model and Comment Model so far. I linked them up. What I did for post and comment models is this: When a user who made a comment deletes the comment, I want the referenced id of that comment in Post collection to be deleted as well from the database. It makes no sense if the comment referenced id remains in the database even though it has no content in it. It can be messy when you have many deleted comments. Comment Model is referenced in Post model. So, when a comment is deleted in Comment collection, the id referenced in the Post collection should be deleted as well. Look at my codes so far:
Comment model
const mongoose = require("mongoose"); //import mongoose to be used
const Schema = mongoose.Schema;
const CommentSchema = new mongoose.Schema(
{
commentdescription:{
type: String,
required: true,
},
author:{
type: Schema.Types.ObjectId,
ref: 'User',
},
postId:{
type: Schema.Types.ObjectId,
ref: "Post",
}
}, {timestamps: true}
);
CommentSchema.pre('update',function(next) {
this.model('Post').update(
{ },
{ "$pull": { "comments": this._id } },
{ "multi": true },
next
);
})
//exporting this schema
module.exports = mongoose.model("Comment", CommentSchema); //the module name is "Post"
I saw a post on this site about doing this with Mongodb middleware. I applied the middleware code inside my Comment Model as you can see above. It is not working. After deleting a comment, the referenced id in the Post collection is still there in the array. I am sure I am not doing it rightly.
Post Model
//creating the user models for the database
const mongoose = require("mongoose"); //import mongoose
const Schema = mongoose.Schema;
const PostSchema = new mongoose.Schema(
{
title:{
type: String,
required: true,
unique: true,
},
description:{
type: String,
required: true,
},
postPhoto:{
type: String,
required:false,
},
username:{
type: Schema.Types.ObjectId,
ref: 'User'
},
categories:{
type: Array,
},
comments: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment',
unique: true,
}]
}, {timestamps: true},
);
//exporting this schema
module.exports = mongoose.model("Post", PostSchema); //the module name is "Post"
Comment deleting code
router.delete("/posts/:id/comment/:id", async (req, res) =>{
try{
const comment = await Comment.findById(req.params.id);
if(comment.author == req.body.author){
try{
await comment.delete()
res.status(200).json("Comment has been deleted")
}catch(err){
console.log(err)
}
}
else{
res.status(401).json("you can only delete your posts")
}
}catch(err){
console.log(err)
}
})
fetching post codes
//Get Post
router.get("/:id", async(req, res)=>{
try{
const post = await Post.findById(req.params.id).populate('username').populate({
path: "comments",
populate: {
path: "author",
}
})
This is a screenshot of the referenced id I am talking about. This particular comment has been deleted but the id is still found in that particular post array where the comment was made.
Your code would be somewhat like this:
deleteCommentById: async function (req, res) {
try {
if (req.params.type === "Comment") {
await postModel.updateMany({ postId: ObjectId(req.params.postId), type: 'Post' }, { $pull: { 'children': ObjectId(req.params.id) } });
await commentModel.updateMany({ postId: ObjectId(req.params.postId) }, { $pull: { 'comments': ObjectId(req.params.id) } });
}
await postModel.findByIdAndDelete(ObjectId(req.params.id));
return res.status(http_status.OK).json({ message: `${req.params.type} deleted successfully` });
} catch (error) {
return res.send("error");
}
}
SO, your code too will be somewhat like the code above which I have written as reference for you.
So instead of this :
CommentSchema.pre('update',function(next) {
Try to use .pre('remove') or .pre('delete') instead of update
I have two schemas - User and Site. User has an array of references to Site. I am trying to return the array of Sites but for some reason its returning the array containing the ObjectIds only.
UserSchema
const UserSchema = new mongoose.Schema({
sites: [{ site: { type: mongoose.Schema.Types.ObjectId, ref: 'Site' },
siteRole: { type: Number } }
]})
module.exports = mongoose.model('User', UserSchema)
SiteSchema
const SiteSchema = new mongoose.Schema({
name: { type: String },
url: { type: String }
})
module.exports = mongoose.model('Site', SiteSchema)
I have tried two ways but both returns the same output i.e. returns the array of site ObjectIds and not the populated version
// 1st try
const user = await User.findById(id)
.populate({
path: 'sites',
model: 'Site',
populate: { path: 'site', model: 'Site' }
}).exec()
// 2nd try
const user = await User.findById(id)
.populate('sites.site')
.exec(function (err, site) {
if (err) return next(err)
if (!site) return res.json(401)
return res.json(site)
})
Both ways returns (array of site ids instead of sites):
[
{
"_id": "60476bf103016cef60b63319",
"siteRole": 11
}
]
I am building an app in which the user adds and deletes objects (Pic) in an array('pics') after registering, but not sure how to dynamically load or populate('pics') to userSchema to automatically render. The user registers on the app with that array originally empty ('pics' = zero), and will create or delete those objects thereafter when logged in.
Following the documentation, I used "await User.find().populate('pics');" to migrate data in index method, but did not work.
Besides, should I include 'pics' key at store method, or userSchema 'pics' should be enough?
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
pics: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Pic"
}
],
});
const picSchema = new mongoose.Schema({
thumbnail: String,
description: String,
dev: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
},
);
const User = mongoose.model('User', userSchema);
const Pic = mongoose.model('Pic', picSchema)
async index(req, res, next) {
const users = await User.find().populate('pics');
res.status(200).json(
devs
);
},
async store(req, res) {
try {
const { name } = req.body;
let user = await User.create({
name,
pics
})
// await user.populate('pics').execPopulate();
res.send({ user })
}
} catch (error) {
res.status(400).send(error);
}
},
I worked a time ago with MongoDB and NodeJS. I think that you have a problem with the definitions. Also, you can read the documentation https://mongoosejs.com/docs/populate.html
You need to define the _id for collections (Schema).
const userSchema = new mongoose.Schema({
_id: new mongoose.Types.ObjectId(),
name: {
type: String,
required: true,
trim: true
},
pics: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Pic"
}
],
});
const picSchema = new mongoose.Schema({
_id: new mongoose.Types.ObjectId(),
thumbnail: String,
description: String,
dev: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
},
);
So, when you create a new User, the _id is completed (you can generate it or it can be generated automatically) and ignore the field pics. When you create a Pic, you need to read the _id of the User and assigned as 'dev', something like:
let pic = new Pic({
thumbnail: '', description: '',
dev: yourUser._id
});
Using this way to create documents, you can use the populate function.
It's possible that i'm just burned out, but I have the following models:
user
const mongoose = require('mongoose');
const validate = require('mongoose-validator');
const Post = require('./post');
let UserSchema = mongoose.Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
email: {
type: String, required: true, lowercase: true, trim: true, unique: true, index: true,
validate: [validate({ validator: 'isEmail', message: 'Invalid Email!' })]
},
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post' }]
})
module.exports = mongoose.model('User', UserSchema);
posts
const _ = require('lodash');
const mongoose = require('mongoose');
const User = require('./user');
let PostSchema = mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
title: { type: String, required: true },
body: { type: String, require: true }
})
PostSchema.post('save', async function (next) {
await User.update({ _id: this.user }, { $push: { posts: this._id } })
return next();
})
module.exports = mongoose.model('Post', PostSchema);
When trying to add a new post, the post save hook runs, but I get the error User.update is not a function (same goes for findOneAndUpdate, findOne, etc).
I can call user.update from the rest of the app without issues, so not sure whats happening here. Both models are in the same directory.
What you missed is that post middleware has the first argument as the "document" and not the next handler:
user.js
const { Schema } = mongoose = require('mongoose');
const userSchema = new Schema({
firstName: String,
lastName: String,
posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }]
});
post.js
const { Schema } = mongoose = require('mongoose');
const User = require('./user');
const postSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User' },
title: String,
body: String
});
// note that first argument is the "document" as in "post" once it was created
postSchema.post('save', async function(doc, next) {
await User.update({ _id: doc.user._id },{ $push: { posts: doc._id } });
next();
});
index.js
const { Schema } = mongoose = require('mongoose');
const User = require('./user');
const Post = require('./post');
const uri = 'mongodb://localhost/posttest';
mongoose.set('debug', true);
mongoose.Promise = global.Promise;
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
let user = await User.create({ firstName: 'Ted', lastName: 'Logan' });
let post = new Post({ user: user._id, title: 'Hi', body: 'Whoa!' });
post = await post.save();
mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
Returns:
Mongoose: users.remove({}, {})
Mongoose: posts.remove({}, {})
Mongoose: users.insertOne({ posts: [], _id: ObjectId("5b0217001b5a55208150cc9b"), firstName: 'Ted', lastName: 'Logan', __v: 0 })
Mongoose: posts.insertOne({ _id: ObjectId("5b0217001b5a55208150cc9c"), user: ObjectId("5b0217001b5a55208150cc9b"), title: 'Hi', body: 'Whoa!', __v: 0 })
Mongoose: users.update({ _id: ObjectId("5b0217001b5a55208150cc9b") }, { '$push': { posts: ObjectId("5b0217001b5a55208150cc9c") } }, {})
Showing that the update fires with the correct detail.
In good design you really should avoid this and simply drop the posts array from the User model. You can always either use a virtual instead:
userSchema.virtual('posts', {
ref: 'Post',
localField: '_id',
foreignField: 'user'
})
Or just get the data via $lookup:
User.aggregate([
{ "$match": { "_id": userId } }
{ "$lookup": {
"from": Post.collection.name,
"localField": "_id",
"foreignField": "user",
"as": "posts"
}}
])
Storing and maintaining arrays of related ObjectId values "on the parent" is kind of an "anti-pattern" and leads to unnecessary overhead such as writing in two places where you only need "one".
Also in general you should be opting for embedding "first", and only considering "referencing" if and when the usage pattern of the application actually demands it. Simply copying the same patterns of an RDBMS with a database engine that was not designed for that is not the best way to utilize it.
I have this schema for a User:
const UserSchema = new Schema({
...
friends: [{
user: {
type: Schema.Types.ObjectId,
ref: 'users'
}
}],
sentRequests: [{
user: {
type: Schema.Types.ObjectId,
ref: 'users'
}
}],
recievedRequests: [{
user: {
type: Schema.Types.ObjectId,
ref: 'users'
}
}]
}
Then when trying to show all the recievedRequests to a page I use this code:
router.get('/friendRequests', (req, res) => {
User.findOne({
_id: req.user.id
})
.populate('recievedRequests.user')
.then(curUser => {
res.render('friends/friendRequests', {
curUser: curUser
});
})
});
where User is the model and req.user.id is the id of the currently logged in user. However, in the webpage whenever I reference curUser.recievedRequests.user it simply returns the id of the user and not the actual user object. I am assuming it has to do with the nested schema reference since I can't see anything else that would cause this issue. Does anyone know of a way to resolve this?
Thanks.