Search code examples
node.jsmongoosemongoose-schemamongoose-populate

How to do multilevel populate in mongoose with nodejs


I would like to create the data for blog post with list of posts, its associated comments with replies along with user details.

I tried nested populate but could do for only one level. Like I get the comments object and in this I need to populate replies and userId. Inside replies I need to populate userId and repliesCount. In this, only the populate that is given at last returns the populated data other fields list only the Ids.

posts.model.js:

const mongoose = require('mongoose')

var ObjectId = mongoose.Schema.Types.ObjectId

let PostsSchema = new mongoose.Schema({
    userId: {
        type: ObjectId,
        ref: 'User'
    },
    celebrityId: {
        type: ObjectId,
        ref: 'CelebrityDetails'
    },
    type: {
        type: Number,
        default: 1
    },
    isForwarded: {
        type: Number,
        default: 0
    },
    originalPostId: {
        type: ObjectId,
        default: null
    },
    title: {
        type: String
    },
    description: {
        type: String
    },
    status: {
        type: Number,
        default: 1
    }
}, {
    timestamps: true,
    collection: 'fan_posts'
})

PostsSchema.virtual('isMyPost', {
    ref: 'User',
    localField: 'userId',
    foreignField: '_id',
    count: true
})

PostsSchema.virtual('comments', {
    ref: 'PostComment',
    localField: '_id',
    foreignField: 'postId'
})

PostsSchema.virtual('commentCount', {
    ref: 'PostComment',
    localField: '_id',
    foreignField: 'postId',
    count: true
})


PostsSchema.set('toObject', { virtuals: true })
PostsSchema.set('toJSON', { virtuals: true })

const Posts = mongoose.model('Posts', PostsSchema)

Posts.consts = {
    STATUS_INACTIVE: 0,
    STATUS_ACTIVE: 1,
    STATUS_DELETED: 2,
    IS_FORWARDED: 1,
    TYPE_TEXT: 1,
    TYPE_IMAGE: 2,
    TYPE_VIDEO: 3,
    TYPE_ASK_TEXT: 4,
    TYPE_ASK_IMAGE: 5,
    TYPE_RATING: 6
}

module.exports = Posts

comments.model.js

const mongoose = require('mongoose')

var ObjectId = mongoose.Schema.Types.ObjectId

let PostCommentSchema = new mongoose.Schema({
    userId: {
        type: ObjectId,
        ref: 'User'
    },
    postId: {
        type: ObjectId,
        ref: 'FanPosts'
    },
    comment: {
        type: String
    },
    isReply: {
        type: Number,
        default: 0
    },
    parentCommentId: {
        type: ObjectId,
        ref: 'PostComment'
    }
}, {
    timestamps: true,
    collection: 'post_comment'
})

PostCommentSchema.set('toObject', { virtuals: true })
PostCommentSchema.set('toJSON', { virtuals: true })

PostCommentSchema.virtual('replies', {
    ref: 'PostComment',
    localField: '_id',
    foreignField: 'parentCommentId'
})

PostCommentSchema.virtual('repliesCount', {
    ref: 'PostComment',
    localField: '_id',
    foreignField: 'parentCommentId',
    count: true,
    justOne: true
})

const PostComment = mongoose.model('PostComment', PostCommentSchema)

PostComment.consts = {
    TYPE_NOT_REPLY: 0,
    TYPE_REPLY: 1
}

module.exports = PostComment

Query:

Posts.find({celebrityId: celebrityId, status: Posts.consts.STATUS_ACTIVE})
.populate({ path: 'userId', select: 'fmId fullName' })
.populate({ path: 'isMyPost', match:{_id: userId} })
.populate({ path: 'comments', match: {isReply: PostComment.consts['TYPE_NOT_REPLY']}, populate: {path: 'userId', select: 'fmId fullName'}, populate: {path: 'replies', match: {isReply: PostComment.consts['TYPE_REPLY']}, populate: {path: 'userId', select: 'fmId fullName'} } })
.populate({ path: 'commentCount'})
.exec(function(err, posts){
    if (err) return res.send({status: status.codes.http['serverError'], message: err})
    return res.send({status: status.codes.http['success'], posts: posts})
})

Result:

{
    "status": 200,
    "posts": [
        {
            "type": 1,
            "isForwarded": 0,
            "originalPostId": null,
            "status": 1,
            "_id": "5d2b16519788076fafe7700c",
            "celebrityId": "5d167ca099a55c2d2494dcf8",
            "post": "hi how are you",
            "userId": {
                "_id": "5d167a397213b127aafb48f3",
                "fmId": "FM499KNWDL",
                "fullName": "Mohideen Abubucker"
            },
            "createdAt": "2019-07-14T11:47:29.863Z",
            "updatedAt": "2019-07-14T11:47:29.863Z",
            "__v": 0,
            "isMyPost": 1,
            "comments": [
                {
                    "isReply": 0,
                    "_id": "5d33721a12aba934e6520f2d",
                    "userId": "5d167a397213b127aafb48f3",
                    "postId": "5d2b16519788076fafe7700c",
                    "comment": "comment 1",
                    "createdAt": "2019-07-20T19:57:14.747Z",
                    "updatedAt": "2019-07-20T19:57:14.747Z",
                    "__v": 0,
                    "replies": [
                        {
                            "isReply": 1,
                            "_id": "5d33724e12aba934e6520f2e",
                            "userId": {
                                "_id": "5d167a397213b127aafb48f3",
                                "fmId": "FM499KNWDL",
                                "fullName": "Mohideen Abubucker"
                            },
                            "postId": "5d2b16519788076fafe7700c",
                            "comment": "comment 1",
                            "parentCommentId": "5d33721a12aba934e6520f2d",
                            "createdAt": "2019-07-20T19:58:06.286Z",
                            "updatedAt": "2019-07-20T19:58:06.286Z",
                            "__v": 0,
                            "id": "5d33724e12aba934e6520f2e"
                        }
                    ],
                    "id": "5d33721a12aba934e6520f2d"
                }
            ],
            "commentCount": 2,
            "id": "5d2b16519788076fafe7700c"
        },


    ]
}

I need to populate userId inside comments object and add repliesCount to the replies object.

The issue is, I can populate only one column. In the query, if you see I would have given populate for both userId and replies. Since replies being the last I am getting the replies data.

I don't know how to populate both replies and userId

Expected:

{
    "status": 200,
    "posts": [
        {
            "type": 1,
            "isForwarded": 0,
            "originalPostId": null,
            "status": 1,
            "_id": "5d2b16519788076fafe7700c",
            "celebrityId": "5d167ca099a55c2d2494dcf8",
            "post": "hi how are you",
            "userId": {
                "_id": "5d167a397213b127aafb48f3",
                "fmId": "FM499KNWDL",
                "fullName": "Mohideen Abubucker"
            },
            "createdAt": "2019-07-14T11:47:29.863Z",
            "updatedAt": "2019-07-14T11:47:29.863Z",
            "__v": 0,
            "isMyPost": 1,
            "comments": [
                {
                    "isReply": 0,
                    "_id": "5d33721a12aba934e6520f2d",
                    "userId": {
                        "_id": "5d167a397213b127aafb48f3",
                        "fmId": "FM499KNWDL",
                        "fullName": "Mohideen Abubucker"
                    },
                    "postId": "5d2b16519788076fafe7700c",
                    "comment": "comment 1",
                    "createdAt": "2019-07-20T19:57:14.747Z",
                    "updatedAt": "2019-07-20T19:57:14.747Z",
                    "__v": 0,
                    "replies": [
                        {
                            "isReply": 1,
                            "_id": "5d33724e12aba934e6520f2e",
                            "userId": {
                                "_id": "5d167a397213b127aafb48f3",
                                "fmId": "FM499KNWDL",
                                "fullName": "Mohideen Abubucker"
                            },
                            "postId": "5d2b16519788076fafe7700c",
                            "comment": "comment 1",
                            "parentCommentId": "5d33721a12aba934e6520f2d",
                            "createdAt": "2019-07-20T19:58:06.286Z",
                            "updatedAt": "2019-07-20T19:58:06.286Z",
                            "__v": 0,
                            "id": "5d33724e12aba934e6520f2e",
                            "repliesCount": 1
                        }
                    ],
                    "id": "5d33721a12aba934e6520f2d"
                }
            ],
            "commentCount": 2,
            "id": "5d2b16519788076fafe7700c"
        },


    ]
}

Solution

  • The problem here:

    .populate({ path: 'comments', match: {isReply: PostComment.consts['TYPE_NOT_REPLY']}, populate: {path: 'userId', select: 'fmId fullName'}, populate: {path: 'replies', match: {isReply: PostComment.consts['TYPE_REPLY']}, populate: {path: 'userId', select: 'fmId fullName'} } })
    

    Your populate parameters:

    { 
        path: 'comments', 
        match: {
            isReply: PostComment.consts['TYPE_NOT_REPLY']
        }, 
        populate: {
            path: 'userId', 
            select: 'fmId fullName'
        },
        populate: {
            path: 'replies', 
            match: {
                isReply: PostComment.consts['TYPE_REPLY']
            }, 
            populate: {
                path: 'userId', 
                select: 'fmId fullName'
            } 
        }
    }
    

    Object literals are a flavor of key value map thus you cannot have multiple keys of the same value within a single level of the object.

    You have 2 keys with the value "populate" at a single level of the object and it looks like only the last one is persisted in the object.

    You can see that here: https://jsfiddle.net/32oe6w8y/1/

    Check the mongoose documentation, I'm sure they have a mechanism to deal with this (populate may take an array.)

    EDT: Based on this you can pass an array into populate: https://stackoverflow.com/a/21100156/2004999

    That may be the solution to your problem.

    Fixed problematic populate parameters:

     { 
            path: 'comments', 
            match: {
                isReply: PostComment.consts['TYPE_NOT_REPLY']
            }, 
            populate: [{
                path: 'userId', 
                select: 'fmId fullName'
            },{
                path: 'replies', 
                match: {
                    isReply: PostComment.consts['TYPE_REPLY']
                }, 
                populate: {
                    path: 'userId', 
                    select: 'fmId fullName'
                } 
            }]
     }