mongodb push if key exists otherwise set array - node.js

If the Investment has deletedDocuments key exists, push the new item to the array, it works fine but
if the deletedDocuments key does not exist in Investment I want to set the item as array, but its setting a [] blank array in db, item values are not saved
const deletedDoc = {
_id: updatedInvestment.id,
docType: 'CADocument',
deletedBy: user,
deletedDate: new Date()
}
if (updatedInvestment.deletedDocuments && updatedInvestment.deletedDocuments.length) {
await Investment.updateOne(
{ "_id": updatedInvestment._id },
{ $push: { "deletedDocuments": deletedDoc } }
);
// this works fine
} else {
await Investment.updateOne(
{ "_id": updatedInvestment._id },
{ $set: { "deletedDocuments": [deletedDoc] } }
// this is setting a blank array [] in the db
);
}

I am thinking about two options here:
Using not in in the filter + push to add the deleted doc to the array.
Using the $addToSet operator.
1:
If you have defined deletedDocuments as an array of objects in your schema you could do something like the following:
await Investment.updateOne(
{
"_id": updatedInvestment._id,
// I'm supposing your deletedDocuments have an _id here
"deletedDocuments._id": { $nin: [deletedDoc._id] }
},
{ $push: { "deletedDocuments": deletedDoc } }
);
This will filter if the deletedDoc does not already exists, if it not, it will push it to the array.
2:
If you have defined deletedDocs as an array of docs ObjectIds instead of objects, you could use the $addToSet operator.
The $addToSet operator adds a value to an array unless the value is already present, in which case $addToSet does nothing to that array.
await Investment.updateOne(
{
"_id": updatedInvestment._id,
},
// Be carefull here, you have to "insert" the deletedDoc's _id if your schema is an ObjectId array
{ $addToSet: { "deletedDocuments": deletedDoc._id } }
);
$addToSet mongodb docs
Not In mongodb docs

Related

MongoDB: Access current value of document when adding element to array via $push

I have collection MyCollection which basically consists of its _id and a string called comment.
This collection should be bulk-updatable.
That's done like this:
for (const obj of inputObjects) {
bulkObjectsToWrite.push({
updateOne: {
filter: { _id: obj._id },
update: {
$set: {
comment: obj.comment
}
}
}
})
}
await MyCollection.bulkWrite(bulkObjectsToWrite)
So far so good.
However, now the requirement is, that a commentHistory should be added which should look like [{"oldValue": "oldValueOfComment", "newValue": "newValueOfComment"}, ...]
I know I need to use $push for adding a new object to the commentHistory array. But how do I access the comment of the document updated right now, i.e. its current value?
I've tried
$push: {
commentHistory: {
newValue: obj.comment,
oldValue: '$comment',
},
},
but to no avail. The string $comment is added hard-coded, instead of the field being accessed.
(Using Mongoose 5.12.10 and Mongo 4.4.18)
You need to use update with aggregate pipeline.
db.collection.update({
"key": 1
},
[
{
$set: {
"comment": "New",
"commentHistory": {
"$concatArrays": [ //concatenate existing history array with new array entry
"$commentHistory",
[
{
"newValue": "New",
"oldValue": "$comment" //referencing the existing value
}
]
]
}
}
}
])
Demo

MongoDB query an array of documents for specific match

I want to search the transactions array looking for a specific match. In this example, by pipedrive_id.
This is what I tried (as per mongodb instructions and this other stack overflow post)
const pipedrive_id = 1677;
const inner_pipedrive_id = 1838;
const result = await Transactions.find({
pipedrive_id,
'transactions': { $elemMatch: { 'pipedrive_id': inner_pipedrive_id } }
});
const result2= await Transactions.find({
'transactions': { $elemMatch: { 'pipedrive_id': inner_pipedrive_id } }
});
const result3 = await Transactions.find({
'transactions.pipedrive_id': inner_pipedrive_id
});
And each result itteration returns all transaction items (all 6 items, instead of 2 [that's how many Mark Smith has in the array).
You can use aggregate to filter out the array. Something like this
You can remove $project if you want all the fields
db.collection.aggregate([
{
$match: {
pipedrive_id: "1677"
}
},
{
$unwind: "$transactions"
},
{
$match: {
"transactions.pipedrive_id": "1838"
}
},
{
$project: {
_id: 0,
pipedrive_id: 1,
transactions: 1
}
}
])
You can check the Mongo playground here.
As the doc, $elemMatch matches documents that contain an array field with at least one element that matches the criteria.
To filter the result inside the array, you will need to use $filter from aggregation
Ref: https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/

Mongoose FindOne returns all children

Is it ok that findOne returns all subdocument in array? I mean isn't it take a long time to execute that, or is there way to get document with only one subDoc ?
my code :
userModel
.find({
{_id: userId,},
{ contacts: { $elemMatch: { userId: contactId } } }
).select('name contacts');
I cant use userModel.contacts.id() cause I don't have id
How can I add a conversation id to this found document
How can I do
const userData = userModel.findById(userId)
.where('contacts')
.elemMatch({ userId: contactId })
and get the result with only one contact (in order not to search for a contact from the entire array)
and simply change relation like
userData.contacts[0].conversation = conversationId;
await userData.save()
How about this:
db.collection.update({
_id: 1
},
{
$set: {
"contacts.$[x].conversation": 3
}
},
{
arrayFilters: [
{
"x.userId": "John"
}
]
})
playground

mongoose schema transform not invoked if document is returned directly from query

I have an endpoint that does an operation such as this:
const pipeline = [
{
$match: {
$and: [
{
$or: [...],
},
],
},
},
{
$group: {
_id : '$someProp',
anotherProp: { $push: '$$ROOT' },
},
},
{ $sort: { date: -1 } },
{ $limit: 10 },
]
const groupedDocs = await MyModel.aggregate(pipeline);
The idea here is that the returned documents look like this:
[
{
_id: 'some value',
anotherProp: [ /* ... array of documents where "someProp" === "some value" */ ],
},
{
_id: 'another value',
anotherProp: [ /* ... array of documents where "someProp" === "another value" */ ],
},
...
]
After getting these results, the endpoint responds with an array containing all the members of anotherProp, like this:
const response = groupedDocs.reduce((docs, group) => docs.concat(group.anotherProp), []);
res.status(200).json(response);
My problem is that the final documents in the response contain the _id field, but I want to rename that field to id. This question addresses this issue, and specifically this answer is what should work, but for some reason the transform function doesn't get invoked. To put it differently, I've tried doing this:
schema.set('toJSON', {
virtuals: true,
transform: function (doc, ret) {
console.log(`transforming toJSON for document ${doc._id}`);
delete ret._id;
},
});
schema.set('toObject', {
virtuals: true,
transform: function (doc, ret) {
console.log(`transforming toObject for document ${doc._id}`);
delete ret._id;
},
});
But the console.log statements are not executed, meaning that the transform function is not getting invoked. So I still get the _id in the response instead of id.
So my question is how can I get id instead of _id in this scenario?
Worth mentioning that toJSON and toObject are invoked (the console.logs show) in other places where I read properties from the documents. Like if I do:
const doc = await MyModel.findById('someId');
const name = doc.name;
res.status(200).json(doc);
The response contains id instead of _id. It's almost like the transform function is invoked once I do anything with the documents, but if I pass the documents directly as they arrive from the database, neither toJSON nor toObject is invoked.
Thanks in advance for your insights. :)
The toJSON and toObject methods won't work here because they don't apply to documents from an aggregation pipeline. Mongoose doesn't convert aggregation docs to mongoose docs, it returns the raw objects returned by the pipeline operation. I ultimately achieved this by adding pipeline stages to first add an id field with the same value as the _id field, then a second stage to remove the _id field. So essentially my pipeline became:
const pipeline = [
{
$match: {
$and: [
{
$or: [...],
},
],
},
},
// change the "_id" to "id"
{ $addFields: { id: '$_id' } },
{ $unset: ['_id'] },
{
$group: {
_id : '$someProp',
anotherProp: { $push: '$$ROOT' },
},
},
{ $sort: { date: -1 } },
{ $limit: 10 },
]
const groupedDocs = await MyModel.aggregate(pipeline);
It is possible to recast the raw objects into mongoose documents after getting them from the aggregate. You just need to transform them back one by one. They will then trigger the toJSON on return.
const document = Model.hydrate(rawObject);
Answer found here:
Cast plain object to mongoose document

Delete a single record from an array of nested document in mongodb

I want to delete just one contact from the contacts array of a particular user
from mongdb dynamically using nodejs
{
_id:ObjectId("532737785878v7676747788"),
firstname:"Mark",
lastname:"Anthony",
email:"xyz#gmail.com",
contacts:[
{
_id:ObjectId("678758478fr7889889)",
firstName:"James",
lastName:"Cole",
phoneNo:"09746"
},
{
_id:ObjectId("678758478fr7889889)"
firstName:"Jane"
lastName:"Doe"
phoneNo:"12345"
}
]
}
I tried this:
User.updateOne(
{email:'xyz#gmail.com', 'contacts._id':678758478fr7889889},
{ $pull : { contacts : { firstName:'Jane', lastName:'Doe', phoneNo:'12345'} } },
{multi:true},
);
I am not getting any error messages and it's not deleting any contact
db.collection.update({
email:'xyz#gmail.com',
contacts: {
$elemMatch: {
"_id": "678758478fr7889889"
}
}
}, {
$pull: {
contacts: {
_id: '678758478fr7889889'
}
}
}
)
Mongoose would use defined schema to create ObjectId's in DB while on writes but it would not use schema on _id(ObjectId's) for find queries, So you've to explicitly say that _id is an ObjectId(). Please have this in your code :
const mongoose = require('mongoose'); // Ignore this if you've already imported.
const ObjectId = mongoose.Types.ObjectId;
// Assuming id is the value you've got in request.
User.updateOne(
{email:'xyz#gmail.com', 'contacts._id':new ObjectId(id)},
{ $pull : { contacts : { firstName:'Jane', lastName:'Doe', phoneNo:'12345'} } });
// you can do the same with `findOneAndUpdate` with options {new: true} which would return updated document, by default it would be false that meant to return old document.
User.findOneAndUpdate(
{email:'xyz#gmail.com', 'contacts._id':new ObjectId(id)},
{ $pull : { contacts : { firstName:'Jane', lastName:'Doe', phoneNo:'12345'} } }, {new : true});

Resources