Mongoose pre save not update field

I have a Video schema like this:

const videoSchema = new mongoose.Schema({
    cover: { // image url
        type: String,
        required: true,
    },
    category: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Category'
    }],
})

when I create new video, I will get cover image from 3rd site and save it to AWS S3, then update the new cover url. I have pre save middleware like this

videoSchema.pre("save", async function save(next) {
    try {
        await transferImagesToS3(this.cover, async (err, resp) => {
            if (err) {
                next()
            }
            if (resp.Location) { // image uploaded to AWS S3 and return new image url here
                this.cover = resp.Location // I set new url here
                next()
            }
        })
    } catch (err) {
        next(err)
    }
}) 

Transfer image function

// transfer image to S3
export const transferImagesToS3 = (imgURL, filename, callback) => {
    request({
        url: imgURL,
        encoding: null
    }, function (err, res, body) {
        if (err) return callback(err, res);

        s3.upload({
            Bucket: 'mybucket',
            Key: `public/images/${filename}.jpeg`,
            cacheControl: 'max-age=604800',
            ContentType: res.headers['content-type'],
            ContentLength: res.headers['content-length'],
            Body: body // buffer
        }, callback)
    })
}

Above code, this.cover is image url from 3rd website. Image uploaded successful but pre save doesn’t updated new image URL. What wrong with this code? Can you help me?

Thank you

Answer

You mix callback style with async/await style. An async function always returns a Promise, in your case async function save(next) will return a promise, and the hook will be “done” right after the promise is resolved.

Mongoose middleware doc.

I guess you expect the hook will wait until transferImagesToS3 is finished, then go to the next process. But actually, your await keyword does not make sense because transferImagesToS3 does not return a Promise (I guest).

We have 2 solutions:

  1. Just use callback style only:
videoSchema.pre("save", function (next) { // remove async
  transferImagesToS3(this.cover, (err, resp) => { // remove await, async
    if (err) {
      next() // next(err) when you want to stop when upload is failed
    }
    if (resp.Location) { // image uploaded to AWS S3 and return new image url here
      this.cover = resp.Location // I set new url here
      next()
    }
  })
})
  1. Convert transferImagesToS3 to return a Promise (I recommend):

I use promisify to convert a callback function to return a Promise.

const util = require('util')

videoSchema.pre("save", async function () { // remove next
  const resp = await util.promisify(transferImagesToS3)(this.cover) // wait
  this.cover = resp.Location
})