Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set field in mongoose document to array length

I have a Mongoose document (Mongoose 5.4.13, mongoDB 4.0.12):

var SkillSchema = new mongoose.Schema({
    skill: { type: String },
    count: { type: Number, default: 0 },
    associatedUsers: [{ type : mongoose.Schema.Types.ObjectId, ref: 'User' }]
});

That I update as follows:

var query = { skill: req.body.skill };
var update = { $addToSet: { associatedUsers: req.params.id } };
            
var options = { upsert: true, new: true, setDefaultsOnInsert: true };

await skillSchema.findOneAndUpdate(query, update, options);

During this update, I would like to also update count to be equal to the length of associatedUsers.

Ideally I want this to happen at the same time as updating the other fields (i.e not in a subsequent update), either via a pre-hook or within findOneAndUpdate.

I've tried using a pre hook after schema definition:

SkillSchema.pre('findOneAndUpdate', async function(){
    console.log("counting associated users");
    this.count = this.associatedUsers.length;
    next();
});

As well as using aggregate in my UPDATE route:

await skillSchema.aggregate([{ $project: { count: { $size: "$associatedUsers" } } } ])

But I can't get either to work.

Does anyone have any suggestions for how I could achieve this?

like image 710
fugu Avatar asked May 07 '19 14:05

fugu


2 Answers

You could use $set like this in 4.2 which supports aggregation pipeline in update.

The first $set stage calculates a associatedUsers based on the previous and new value. $setUnion to keep the distinct associatedUsers values.

The second $set stage calculates tally based on the associatedUsers calculated in the previous stage.$size to calculate the length of associatedUsers values.

var query = {skill: req.body.skill};
var update = [{ $set: { "associatedUsers":{"$setUnion":[{"$ifNull":["$associatedUsers",[]]}, [req.params.id]] }}}, {$set:{tally:{ $size: "$associatedUsers" }}}];
var options = { upsert: true, new: true, setDefaultsOnInsert: true };
await skillSchema.findOneAndUpdate(query, update, options)

If any argument resolves to a value of null or refers to a field that is missing, $setUnion returns null. So just needed to safeguard our operation with $ifNull

like image 127
s7vr Avatar answered Nov 03 '22 15:11

s7vr


About tally and associatedUsers.length

// define your schema object
var schemaObj = {
  skill: { type: String },
  associatedUsers: { type: Array }
};

// get the length of users
var lengthOfAsUsers = schemaObj.associatedUsers.length;

// add tally to schema object and set default to the length of users
schemaObj.tally = { type: Number, default: lengthOfAsUsers };

// and pass your schema object to mongoose.Schema
var SkillSchema = new mongoose.Schema(schemaObj);

module.exports = SkillSchema;

EDIT you can update tally subsequently, but recommended solution would be to use this method https://mongoosejs.com/docs/populate.html

const id = "nameSomeId";

SkillSchema.find({ _id: id }).then(resp => {
  const tallyToUpdate = resp.associatedUsers.length;
  SkillSchema.findOneAndUpdate({ _id: id }, { tally: tallyToUpdate }).then(
    resp => {
      console.log(resp);
    }
  );
});
like image 39
JozeV Avatar answered Nov 03 '22 17:11

JozeV