node.jsvalidationmongooseobject-object-mapping

Missing values in Mongoose pre-validate when mapping object lists to string lists


I'm trying to create a new record using data from the request body. The request body data looks like this:

{
  extras: [{value: "string"}, {value: "string"}, {value: "string"}],
  skills: [{value: "string"}, {value: "string"}]
}

These properties contain lists of objects. To create a new record in my post request handler, I'm using the following code:

const record = new Record(req.body);
try {
  const newRecord = await record.save();
  res.status(200).json(newRecord);
} catch (err) {
  res.status(400).json({ message: err.message });
}

However, my Mongoose schema expects these properties to be lists of strings. To convert the object values to strings before saving the record to the database, I've added a pre-validate hook. Here's my code:

const recordSchema = new Schema(
  {
    extras: [String],
    skills: [String]
  },
  {
    timestamps: true
  }
)

recordSchema.pre('validate', function (next) {
  console.log(this);
  this.extras = this.extras.map(v => v.value);
  this.skills = this.skills.map(v => v.value);
  return next();
});

When I log this in the pre-validate hook, I see that the values are missing:

{
  extras: [],
  skills: []
}

How can I ensure that the values are properly mapped from the objects to strings before saving the record?


Solution

  • Any key/val set that does not exist in your schema is always ignored.

    The [{value: 'string'}] does not match the extras key in the schema, it will be ignored.

    import mongoose from 'mongoose';
    import { config } from '../../config';
    
    mongoose.set('debug', true);
    
    const recordSchema = new mongoose.Schema({
        extras: [String],
        skills: [String],
    });
    recordSchema.pre('validate', function (next) {
        console.log('this: ', this);
        next();
    });
    const Record = mongoose.model('record', recordSchema);
    
    (async function main() {
        try {
            await mongoose.connect(config.MONGODB_URI);
            await Promise.all([Record].map((m) => m.collection.drop()));
            // seed
            const body = {
                extras: [{ value: 'a' }, { value: 'b' }, { value: 'c' }],
                skills: [{ value: 'x' }, { value: 'y' }],
            };
            const r1 = new Record(body);
            console.log('r1: ', r1);
    
            const r2 = new Record({ extras: body.extras.map((v) => v.value), skills: body.skills.map((v) => v.value) });
            console.log('r2: ', r2);
            await r2.save();
        } catch (error) {
            console.error(error);
        } finally {
            await mongoose.connection.close();
        }
    })();
    

    Debug logs:

    r1:  {
      extras: [],
      skills: [],
      _id: new ObjectId("6499913304a2e6257b3b5186")
    }
    r2:  {
      extras: [ 'a', 'b', 'c' ],
      skills: [ 'x', 'y' ],
      _id: new ObjectId("6499913304a2e6257b3b5187")
    }
    this:  {
      extras: [ 'a', 'b', 'c' ],
      skills: [ 'x', 'y' ],
      _id: new ObjectId("6499913304a2e6257b3b5187")
    }
    Mongoose: records.insertOne({ extras: [ 'a', 'b', 'c' ], skills: [ 'x', 'y' ], _id: ObjectId("6499913304a2e6257b3b5187"), __v: 0}, {})
    

    Compare the r1 document and the r2 document. You can't transform/process the invalid keys/values in pre('validate') middleware because mongoose will ignore them. So, try to process the req.body to match the keys/values in the schema and use the proceed data to create the model instance.