post middleware in mongoose not atomizing operation? still creates document even though next(err) is called

190 Views Asked by At

I have a model, let's call it Client and i have another model called Accounts. They are from difference collections, a client can have many different accounts. I reference the accounts within the client doc, as well as referencing back to the client from the account doc.

const Client = new mongoose.Schema({
    accounts: [{
          type:mongoose.ObjectId,
          ref: 'Accounts',
      }],
    other....
})

const Accounts = new mongoose.Schema({
    name:String,
    clientID: mongoose.ObjectId
})

So as we can see they reference each other. I'm doing this for easy access to populating the account and such when requesting client info.

What im trying to do is when i create a new client, i also want to create a new default account and reference it in the accounts array. I tried using a pre hook when i create my new Client to create a new Account, however that doesn't update the Client account array with the newly created Account doc _id. I've tried using this.update()

Client.pre('save',async function(next,args){
  if(this.isNew){
    await Accounts.create({clientID:this._id})
        .then(async doc=>{
            console.log(doc) // this logs my account doc just fine, which means it got created
            await this.update($push:{accounts:doc._id) // this doesnt seem to do anything
        })
        .catch(err=>next(err)
  }
  next()
}

So the pre hook almost did what i wanted it to do, but I can't figure out a way to update my newly created Client doc with the info from the newly created Account doc. It creates the Client doc, and it creates the Account doc. And the beauty of it is if I have an error when creating the Account doc, then since it is atomized then the Client doesn't get created. But alas, no updated accounts array...

So instead, I tried putting it into a post hook.

Client.pre('save',async function(doc, next){
    await Accounts.create({clientID:doc._id})
        .then(async acc=>{
           await doc.update({$push:{accounts:[acc._id]}}) 
        }).catch(err=>next(err)
}

And hey! this works!...kinda... I can create a Client document which creates an Account document, and then updates the Client to include the Account _id in its accounts array.

BUT!!! The issue im having with this approach is that it doesnt seem to be atomizing the operation. So if i deliberately make the account creation fail (by for example passing it a non ObjectID argument), then it calls next(err) which in my http request properly returns the error message, and even says that the operation failed. But in my database, the Client document still got created, unlike in the pre-hook where it stops the whole operation, in the post hook it does not 'undo' the creation of the Client.

SUMMARY AND SOLUTIONS

Basically I need a way to update a brand new doc inside of its pre.('save') hook so it will store any changed data i processed inside the hook.

Or some way to guarantee the atomization of the operation if i use a post hook to update the new doc.

Other things i've tried:

I also tried using save() inside the pre hook after creating the Account doc, but this resulted in an loop that maxed out the doc memory since it just became recursive

I tried using a pre-hook on the Accounts model so it would reference back to the Client model and update it, but this gives me both issues together. It does not update the new client doc (since it's technically not queryable yet) AND if the account creation fails, it still creates the Client.

Sorry for the long question, I appreciate any feedback or recommendations to fix this issue or different approach to achieve my goal. If you made it this far, thanks for reading!

1

There are 1 best solutions below

0
xunux On

My question was built up of a few questions, but i want to post the solution.

While i still don't know how to guarantee that an error in a post hook will make the whole operation behave atomically, the solution to this was quite simple.

Inside the pre hook,to modify the accounts array i just had to push() into it, no need to try using this.set or this.update or any other actual query, just direct modification of this

{
  //inside Client pre hook
  //create account doc
 await Accounts.create(...).then(doc=>{
      this.accounts.push(doc._id)
    }).catch(err=>next(err)
}