Chrillewoodz
Chrillewoodz

Reputation: 28328

How to count number of ObjectId occurrences in an array of ObjectIds' and attach the result to each document that is being returned?

I have a Tag schema:

const TagSchema = new Schema({
  label: {type: String, unique: true, minlength: 2},
  updatedAt: {type: Date, default: null},
  createdAt: {type: Date, default: new Date(), required: true},
  createdBy: {type: mongoose.Schema.Types.ObjectId, ref: 'Account', default: null}
});

const Tag = mongoose.model('Tag', TagSchema);

Then I have a Page schema:

const PageSchema = new Schema({
  tags: {type: [{type: mongoose.Schema.Types.ObjectId, ref: 'Tag'}], default: [], maxlength: 5}
});

const Page = mongoose.model('Page', PageSchema);

As you can see it contains a Tag reference in its tags array.

Now what I need to do is when I fetch all tags via /tags, I also need to count the number of times each tag is used in all Pages.

So if a Tag is used 2times across all Pages, it should set an .occurrences property on the returned tag. For example this would be a response from /tags:

[
 {_id: '192398dsf7asdsdds8f7d', label: 'tag 1', updatedAt: '20170102', createdAt: '20170101', createdBy: '01238198dsad8s7d78ad7', occurrences: 2},
 {_id: '19239asdasd8dsf7ds8f7d', label: 'tag 2', updatedAt: '20170102', createdAt: '20170101', createdBy: '01238198dsad8s7d78ad7', occurrences: 1},
 {_id: '192398dsf7zxccxads8f7d', label: 'tag 1', updatedAt: '20170102', createdAt: '20170101', createdBy: '01238198dsad8s7d78ad7', occurrences: 5},
]

I would have imagined that I could achieve this pretty easily in a mongoose pre('find') hook like this:

TagSchema.pre('find', function() {

  Page.count({tags: this._id}, (err, total) => {
    this.occurrences = total;
  });
});

However, there are two problems here:

So I guess that the way I am going about this is completely wrong.

Since I am a novice in MongoDB maybe someone can provide me with a better, working, solution to my problem?

Upvotes: 1

Views: 144

Answers (2)

Shubham
Shubham

Reputation: 1426

db.Page.aggregate([ 
{
  $unwind:"$tags"
},
{
  $group:{ 
    _id:"$tags", 
    occurrences:{$sum:1}
  }
},
{
  $lookup:{ //get data for each tag
    from:"tags",
    localField:"_id",
    foreignField:"_id",
    as:"tagsData"
  }
},
{
$project:{
  tagData:{$arrayElemAt: [ "$tagsData", 0 ]},
  occurrences:1
}
}
{
  $addFields:{ 
    "tagData._id":"$_id",
    "tagData.occurrences":"$occurrences"
  }
},
{
   $replaceRoot: { newRoot: "$tagData" }
}
])

Upvotes: 1

madllamas
madllamas

Reputation: 518

Ok first, you need getQuery() this allows you to access the properties that you are finding the tag with. so if you find the tag by _id, you will have access to it in the pre hook by this.getQuery()._id

Second, you are going to need to use the "Parallel" middleware, can be found in the docs. This allows the find method to wait until the done function is called, so it is gonna wait until the tag is updated with the new occurrences number.

As the object can no be accessed inn the pre hook as it is yet to be found, you are going to have to use findOneAndUpdate method in the pre hook.

So the code should look like :

The find method :

Tag.find({ _id: "foo"}, function (err, foundTag) {
  console.log(foundTag)
})

The pre hook :

TagSchema.pre('find', true, function (next, done) {
  mongoose.models['Page'].count({ tags: this.getQuery()._id }, (err, total) => {
    mongoose.models['Tag'].findOneAndUpdate({ _id: this.getQuery()._id }, { occurrences: total }, function () {
      next()
      done()
    })
  });
});

Upvotes: 0

Related Questions