Home > Software design >  Mongoose populate won't work after "post" middleware
Mongoose populate won't work after "post" middleware

Time:09-20

Let's say I have the following schemas:

parent.ts

export const ParentSchema = new Schema<IParent>(
  {
    children: {
      type: [
        {
          type: Schema.Types.ObjectId,
          ref: 'Child',
        },
      ],
      default: [],
    },
  },
  { timestamps: true }
);

export default model('Parent', ParentSchema, 'Parent');

child.ts

import Parent from './parent';

const ChildSchema = new Schema<IChild>(
  {
    parent: {
      type: Schema.Types.ObjectId,
      ref: 'Parent',
      required: true,
    },
  },
  { timestamps: true }
);

ChildSchema.post('save', async function () {
  const { _id, parent: parentId } = this;
  console.log('pre findByIdAndUpdate');
  await Parent.findByIdAndUpdate(parentId, { $push: { children: _id } });
  console.log('post findByIdAndUpdate');
});

export default model('Child', ChildSchema, 'Child');

The Parent schema is actually a wrapper of multiple children, so it only makes sense to have a Parent if one Child is being created as well. So, I have this function:

parent-service.ts

async store() {
  const newParent = await new Parent().save();
  const newChild = await new Child({ parent: newParent._id }).save();
  await newParent.populate('children');
  // console.log(await Parent.findById(newParent._id));
  return newParent;
}

I would expect that the returned object would be something like:

{
  "_id": "1234567890abcdef",
  "children": [
    {
       "_id": "fedcba0987654321",
       "parent": "1234567890abcdef"
    }
  ]
}

However, this is what is being returned by calling await store():

{
  "_id": "1234567890abcdef",
  "children": []
}

The console.log on post middleware at child.ts are printed, and if I remove the console.log commented out at parent-service.ts, it shows that indeed the middleware worked as intended and the id was added to the array of children.

Is this behavior expected, is it a bug?

What I've tried:

  1. Using SchemaTypes.ObjectId instead of Schema.Types.ObjectId;
  2. Using updateOne instead of findByIdAndUpdate on the middleware;

I'm currently using, as a workaround, a spread on newParent putting children: [newChild], but I'd like to populate actually get the updated version of my document.

Thanks in advance.

CodePudding user response:

I think it is expected behavior, the reason being this:

async store() {
  const newParent = await new Parent().save(); <-- S1
  const newChild = await new Child({ parent: newParent._id }).save(); <-- S2
  await newParent.populate('children'); <-- S3
  // console.log(await Parent.findById(newParent._id));
  return newParent;
}

When S1 executes, newParent points to the object, whose value is this

{
  "_id": "1234567890abcdef",
  "children": []
}

After this, the child is created and the save middleware updates the parent using findByIdAndUpdate or updateOne. But the thing to note here is, that the updated parent document returned by these functions is not assigned to any variables, let alone newParent, also it's not trivial to assign it to newParent as well. That's why when you cal populate method in S3, nothing gets populated, because the value referred to by newParent, still has children as an empty array.

Now to fix this, you can try this:

  1. You can basically call findByIdAndUpdate, within the store method itself, and store the updated parent object in the newParent variable.

  2. You can also try overriding the children array of newParent, in the store method by storing the newly created child _id in it and then call populate (not recommended).

  3. Simply call findById for the parent once the child is saved (best option).

  • Related