fugu
fugu

Reputation: 6568

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?

Upvotes: 9

Views: 3746

Answers (4)

s7vr
s7vr

Reputation: 75924

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

Upvotes: 6

sushant mehta
sushant mehta

Reputation: 1274

The solution I have will only work on mongodb v 4.2 as it has option to use aggregate in the update and will only need one query as:

skillSchemafindOneAndUpdate(


 {skill:"art"},
   [
     { $set: { 
       associatedUsers:{
         $cond:{
            if: {$gte: [{$indexOfArray: ["$associatedUsers", mongoose.Types.ObjectId(req.params.id)]}, 0]},
              then: "$associatedUsers",
              else: { $cond:{
                if: { $isArray: "$associatedUsers" },
                then: {$concatArrays:["$associatedUsers",[mongoose.Types.ObjectId(req.params.id)]]},
                else: [mongoose.Types.ObjectId(req.params.id)]
           }}
           }
      }}},
      {$set:{
        associatedUsers:"$associatedUsers",
        tally:{$size:"$associatedUsers"},
      }}
   ],
   {upsert:true,new:true}
)

ref: https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline

Upvotes: 1

Rıdvan Can
Rıdvan Can

Reputation: 89

The "Group" field does not appear in the schema. On MongoDB Shell, these codes will work.

However, Mongoose will also give an error because the schema is validated.

Is the "Group" field a dynamic field? I think the problem with the schema will be solved.

var mongoose = require("mongoose");

var SkillSchema = new mongoose.Schema({
    skill: { type: String },
    tally: { type: Number, default: 0 },
    associatedUsers: { type: Array },
    group: { type: Array }
 });

Upvotes: 0

JozeV
JozeV

Reputation: 696

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);
    }
  );
});

Upvotes: 2

Related Questions