Search code examples
javascriptmongodbmongoose

How to trigger a function for each document insertion with Mongoose?


I want to trigger a function each time an element is inserted/created in a collection, not when it's updated. (And I need the ID of the element created)

Here's what I did:

schema.pre("save", function() {
    this.$locals.wasNew = this.isNew;
});

schema.post("save", function() {
    if (this.$locals.wasNew) {
        console.log(`User ${this.id} created!`);
    }
});

It works with calls like this one:

await UserModel.create({
    mail
});

But not with calls like this one:

const user = await UserModel.findOneAndUpdate(
    { mail },
    { $set: { mail } },
    { new: true, upsert: true }
);

It seems like upsert does not trigger the save hook with the isNew boolean set to true.

Am I doing it wrong? Missing something? Or is there an alternative way of doing it?


Solution

  • It is not possible to have the same code in document.save hooks executed on the query.findOneAndUpdate as well. The reason is that the hooks in document.save will not be executed for query methods. For more, please see link 1 below.

    However, there are following workarounds:

    1. Use the two separate methods findById and save, the reasoning is that findOneAndUpdate is effectively doing the same. You may be able to see the original discussion on these lines here, reference link 2.

    2. Using change streams, reference link 3.

    3. As @joe has commented, using includeResultMetadata, reference link 4.

    Please also see the sample code below for the options 2 and 3.

    const mongoose = require('/mongoose');
    const { Schema } = mongoose;
    
    run().catch((error) => console.log(error));
    async function run() {
      await mongoose.connect('mongodb://127.0.0.1:27108', { replicaSet: 'rs' });
      const SomeModel = mongoose.model('Somemodel', new Schema({ key1: String }));
      await SomeModel.deleteMany();
    
      SomeModel.watch().on('change', (data) => {
        if (data.operationType == 'insert') {
          console.log(`change stream, new document, _id: ${data.fullDocument._id}`);
        }
      });
    
      await new SomeModel({ key1: 'key1 value 1' }).save();
      const doc2 = await SomeModel.findOneAndUpdate(
        { key1: 'xyz' },
        { key1: 'key1 value2' },
        { new: true, upsert: true, includeResultMetadata: true }
      );
    
      !doc2.lastErrorObject.updatedExisting &&
        console.log(`ResultMetadata, new document, _id: ${doc2.value._id}`);
    }
    

    OUTPUT:

    change stream, new document, _id: 6643275fdb709107184e999a
    ResultMetadata, new document, _id: 6643275f7550f5d65e7ba6a3
    change stream, new document, _id: 6643275f7550f5d65e7ba6a3
    

    Links

    1. Notes on findAndUpdate() and Query Middleware

    2. pre, post middleware are not executed on findByIdAndUpdate #964

    3. Change streams

    4. Using includeResultMetadata