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:
- Using
SchemaTypes.ObjectId
instead ofSchema.Types.ObjectId
; - Using
updateOne
instead offindByIdAndUpdate
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:
You can basically call
findByIdAndUpdate
, within thestore
method itself, and store the updated parent object in thenewParent
variable.You can also try overriding the
children
array ofnewParent
, in thestore
method by storing the newly created child_id
in it and then call populate (not recommended).Simply call
findById
for the parent once the child is saved (best option).