Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Populating virtual count field returns the sum of all referencing documents. v5.13.0 #11307

Closed
zoran-php opened this issue Feb 1, 2022 · 4 comments
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@zoran-php
Copy link

I have Comment and Report models. Comments can have multiple replies stored in field replies which is array of references.
When User reports the comment/reply new Report document is stored in Report collection with reference to the Comment.
I have 2 virtuals in Comment schema, reportCount (number of reports for that comment) and reportReplyCount (number of reports for comment replies).

CommentSchema.virtual('reportCount', {
    ref: 'Report',
    localField: '_id',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
});

CommentSchema.virtual('reportReplyCount', {
    ref: 'Report',
    localField: 'replies',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
    options: {
        match: {
            reportType: 'Comment',
        },
    },
});

The reportCount works fine, returns the number of reports for that Comment. But the reportReplyCount does not. It returns total number of references in the reference array (replies) instead of number of reports for those referenced replies. Version of mongoose is 5.13.0. That means if I have 4 replies in that replies array it will return number 4. But it should return sum of all reports for those references.

Comment Schema

const mongoose = require('mongoose');
const Populate = require('../utils/autopopulate');
const Hashtag = require('./Hashtag');

const CommentSchema = new mongoose.Schema(
    {
        content: {
            type: String,
            trim: true,
            maxLength: 2048,
        },
        createdAt: {
            type: Date,
            default: Date.now,
        },
        channel: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Channel',
            required: true,
        },
        post: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Post',
            required: true,
        },
        likes: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'User',
                required: false,
            },
        ],
        numberOfLikes: {
            type: Number,
            default: 0,
        },
        authorModelType: {
            type: String,
            enum: ['Admin', 'User'],
        },
        author: {
            type: mongoose.Schema.Types.ObjectId,
            refPath: 'authorModelType',
        },
        comment: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Comment',
            required: false,
        },
        replies: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'Comment',
                required: false,
            },
        ],
        isReply: {
            type: Boolean,
            default: false,
        },
        isChannelComment: {
            type: Boolean,
            default: false,
        },
        edited: {
            type: Boolean,
            default: false,
        },
        deleted: {
            type: Boolean,
            default: false,
        },
        deletedAt: {
            type: Date,
            required: false,
        },
        space: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Space',
            required: false,
        },
        trendingCount: {
            type: Number,
            default: 0,
            min: 0,
        },
        hashtags: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'Hashtag',
            },
        ],
    },
    { toJSON: { virtuals: true }, toObject: { virtuals: true } }
);

CommentSchema.virtual('reportCount', {
    ref: 'Report',
    localField: '_id',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
});

// TODO: fix reportReplyCount
CommentSchema.virtual('reportReplyCount', {
    ref: 'Report',
    localField: 'replies',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
    options: {
        match: {
            reportType: 'Comment',
        },
    },
});

CommentSchema.pre(
    'findOne',
    Populate('author', 'reportCount', 'reportReplyCount')
).pre('find', Populate('author', 'reportCount', 'reportReplyCount'));



module.exports = mongoose.model('Comment', CommentSchema);

Report Schema

const mongoose = require('mongoose');
const Populate = require('../utils/autopopulate');

const ReportSchema = new mongoose.Schema({
    reason: {
        type: String,
        trim: true,
        enum: [
            'BULLYING',
            'FAKE NEWS',
            'RACISM',
            'SCAM',
            'SEXIST',
            'SEXUALLY INNAPROPRIATE',
            'SPAM',
            'OTHER',
        ],
    },
    description: {
        type: String,
        trim: true,
        required: false,
        maxLength: 280,
    },
    reporter: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true,
    },
    createdAt: {
        type: Date,
        default: Date.now,
    },
    reportType: {
        type: String,
        enum: ['Post', 'Comment'],
        required: true,
    },
    reportModel: {
        type: mongoose.Schema.Types.ObjectId,
        refPath: 'reportType',
        required: true,
    },
    reportedUser: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true,
    },
    space: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Space',
        required: false,
    },
    channel: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Channel',
        required: false,
    },
    post: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Post',
        required: false,
    },
});

// Create bootcamp slug from the name
ReportSchema.pre(
    'findOne',
    Populate('reporter', 'reportModel', 'reportedUser')
).pre('find', Populate('reporter', 'reportModel', 'reportedUser'));

module.exports = mongoose.model('Report', ReportSchema);
@IslandRhythms IslandRhythms added the needs clarification This issue doesn't have enough information to be actionable. Close after 14 days of inactivity label Feb 3, 2022
@IslandRhythms
Copy link
Collaborator

So what you're saying is if there are 4 reports in the array and each report has 3 reports, it should return 12?

@zoran-php
Copy link
Author

So what you're saying is if there are 4 reports in the array and each report has 3 reports, it should return 12?

Yes, it should. It works well for the first case reportCount, it counts the number of reports for that comment.
But it does not count the total number of reports for the comment replies (stored in replies array). It counts and returns a total number of children, regardless of the reports. It ignores reports completely. And the only difference is that in the second case localField is an array of refs.

@vkarpov15 vkarpov15 added this to the 6.2.4 milestone Feb 6, 2022
@vkarpov15 vkarpov15 added needs repro script Maybe a bug, but no repro script. The issue reporter should create a script that demos the issue and removed needs clarification This issue doesn't have enough information to be actionable. Close after 14 days of inactivity labels Feb 6, 2022
@IslandRhythms IslandRhythms added needs clarification This issue doesn't have enough information to be actionable. Close after 14 days of inactivity and removed needs repro script Maybe a bug, but no repro script. The issue reporter should create a script that demos the issue labels Feb 8, 2022
@IslandRhythms
Copy link
Collaborator

So after attempting to reproduce this error, I'm a bit confused as to how you would expect the number 12. reportType and reportModel are both single entries.

const mongoose = require('mongoose');


const ReportSchema = new mongoose.Schema({
    reason: {
        type: String,
        trim: true,
        enum: [
            'BULLYING',
            'FAKE NEWS',
            'RACISM',
            'SCAM',
            'SEXIST',
            'SEXUALLY INNAPROPRIATE',
            'SPAM',
            'OTHER',
        ],
    },
    description: {
        type: String,
        trim: true,
        required: false,
        maxLength: 280,
    },
    createdAt: {
        type: Date,
        default: Date.now,
    },
    reportType: {
        type: String,
        enum: ['Post', 'Comment'],
        required: true,
    },
    reportModel: {
        type: mongoose.Schema.Types.ObjectId,
        refPath: 'reportType',
        required: true,
    },
});


const CommentSchema = new mongoose.Schema(
    {
        content: {
            type: String,
            trim: true,
            maxLength: 2048,
        },
        createdAt: {
            type: Date,
            default: Date.now,
        },
        likes: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'User',
                required: false,
            },
        ],
        numberOfLikes: {
            type: Number,
            default: 0,
        },
        comment: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Comment',
            required: false,
        },
        replies: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'Comment',
                required: false,
            },
        ],
        isReply: {
            type: Boolean,
            default: false,
        },
        isChannelComment: {
            type: Boolean,
            default: false,
        },
        edited: {
            type: Boolean,
            default: false,
        },
        deleted: {
            type: Boolean,
            default: false,
        },
        deletedAt: {
            type: Date,
            required: false,
        },
        space: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Space',
            required: false,
        },
        trendingCount: {
            type: Number,
            default: 0,
            min: 0,
        },
    },
    { toJSON: { virtuals: true }, toObject: { virtuals: true } }
);

CommentSchema.virtual('reportCount', {
    ref: 'Report',
    localField: '_id',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
});

// TODO: fix reportReplyCount
CommentSchema.virtual('reportReplyCount', {
    ref: 'Report',
    localField: 'replies',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
    options: {
        match: {
            reportType: 'Comment',
        },
    },
});



const Comment = mongoose.model('Comment', CommentSchema);
const Report = mongoose.model('Report', ReportSchema);

async function run() {
    await mongoose.connect('mongodb://localhost:27017');
    await mongoose.connection.dropDatabase();

    const comment = await Comment.create({
        content: 'Spamming Spamerson'
    });

    const otherComment = await Comment.create({
        content: 'Spamming Springsteen'
    });

    const differentComment = await Comment.create({
        content: 'Spamming Sanderson'
    });

    const finalComment = await Comment.create({
        content: 'Spamming Sam'
    });

    const report = await Report.create({
        reason: 'SPAM',
        reportType: 'Comment',
        reportModel: comment._id
    });

    const secondReport = await Report.create({
        reason: 'SPAM',
        reportType: 'Comment',
        reportModel: comment._id
    });

    const thirdReport = await Report.create({
        reason: 'SPAM',
        reportType: 'Comment',
        reportModel: comment._id
    });

    await Comment.findOneAndUpdate({_id: comment._id}, {replies: [otherComment._id, finalComment._id, differentComment._id]});
    await Comment.findOneAndUpdate({_id: otherComment._id}, {replies: [comment._id, finalComment._id, differentComment._id]})
    await Comment.findOneAndUpdate({_id: finalComment._id}, {replies: [otherComment._id, finalComment._id, differentComment._id]})


    const test = await Comment.findOne({_id: comment._id}).populate('reportReplyCount').populate('reportCount');
    console.log(test);
}

run();

@vkarpov15 vkarpov15 modified the milestones: 6.2.4, 6.2.5 Feb 24, 2022
@vkarpov15 vkarpov15 added confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. and removed needs clarification This issue doesn't have enough information to be actionable. Close after 14 days of inactivity labels Mar 5, 2022
@vkarpov15
Copy link
Collaborator

You're right that reportReplyCount works incorrectly. In v6.2.5, we'll make it so that it instead returns an array, 1 for each reply:

const mongoose = require('mongoose');
mongoose.set('debug', true);


const ReportSchema = new mongoose.Schema({
    reason: {
        type: String,
        trim: true,
        enum: [
            'BULLYING',
            'FAKE NEWS',
            'RACISM',
            'SCAM',
            'SEXIST',
            'SEXUALLY INNAPROPRIATE',
            'SPAM',
            'OTHER',
        ],
    },
    description: {
        type: String,
        trim: true,
        required: false,
        maxLength: 280,
    },
    createdAt: {
        type: Date,
        default: Date.now,
    },
    reportType: {
        type: String,
        enum: ['Post', 'Comment'],
        required: true,
    },
    reportModel: {
        type: mongoose.Schema.Types.ObjectId,
        refPath: 'reportType',
        required: true,
    },
});

const CommentSchema = new mongoose.Schema(
    {
        content: {
            type: String,
            trim: true,
            maxLength: 2048,
        },
        createdAt: {
            type: Date,
            default: Date.now,
        },
        likes: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'User',
                required: false,
            },
        ],
        numberOfLikes: {
            type: Number,
            default: 0,
        },
        comment: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Comment',
            required: false,
        },
        replies: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'Comment',
                required: false,
            },
        ],
    },
    { toJSON: { virtuals: true }, toObject: { virtuals: true } }
);

CommentSchema.virtual('reportCount', {
    ref: 'Report',
    localField: '_id',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
});

// TODO: fix reportReplyCount
CommentSchema.virtual('reportReplyCount', {
    ref: 'Report',
    localField: 'replies',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
    options: {
        match: {
            reportType: 'Comment',
        },
    },
});

const Comment = mongoose.model('Comment', CommentSchema);
const Report = mongoose.model('Report', ReportSchema);

async function run() {
    await mongoose.connect('mongodb://localhost:27017');
    await mongoose.connection.dropDatabase();

    const comment = await Comment.create({
        content: 'Spamming Spamerson'
    });

    const otherComment = await Comment.create({
        content: 'Spamming Springsteen'
    });

    const differentComment = await Comment.create({
        content: 'Spamming Sanderson'
    });

    const finalComment = await Comment.create({
        content: 'Spamming Sam'
    });

    const report = await Report.create({
        reason: 'SPAM',
        reportType: 'Comment',
        reportModel: comment._id
    });

    const secondReport = await Report.create({
        reason: 'SPAM',
        reportType: 'Comment',
        reportModel: comment._id
    });

    const thirdReport = await Report.create({
        reason: 'SPAM',
        reportType: 'Post',
        reportModel: new mongoose.Types.ObjectId()
    });

    await Comment.findOneAndUpdate({_id: comment._id}, {replies: [otherComment._id, finalComment._id, differentComment._id, comment._id]});
    await Comment.findOneAndUpdate({_id: otherComment._id}, {replies: [comment._id, finalComment._id, differentComment._id]})
    await Comment.findOneAndUpdate({_id: finalComment._id}, {replies: [otherComment._id, finalComment._id, differentComment._id]})

    const test = await Comment.findOne({_id: comment._id}).populate('reportReplyCount');
    console.log(test.reportReplyCount); // [0, 0, 0, 2]
}

run();

If you want to sum up [0, 0, 0, 2] you can just do:

CommentSchema.virtual('reportReplyCount', {
    ref: 'Report',
    localField: 'replies',
    foreignField: 'reportModel',
    justOne: false,
    count: true,
    options: {
        match: {
            reportType: 'Comment',
        },
    },
}).get(arr => Array.isArray(arr) ? arr.reduce((sum, el) => sum + el, 0) : 0);

Is there anything blocking you from upgrading to Mongoose 6?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Projects
None yet
Development

No branches or pull requests

3 participants