upsert with $set if not null mongodb - node.js

Suppose a I have a collection of document structure.
{
uuid: "157071f4-2624-4c49-8735-bd345425a16b",
price: {
item1: 20,
item2: 30
}
}
Now I want to upsert data based on uuid, using something like
await Coll.findOneAndUpdate({uuid}, {$set: {[`price.${itemNo}`]: 40}, {upsert: true}}
My issue is that $set here update values even in cases where itemNo already exists. And if I put a check on query, something like,
{uuid, [`price.${itemNo}`]: {$exists: 0}}
then it creates a new document due to upsert even if uuid exists.
My end goal is to achieve upsert if uuid doesn't exist and and if uuid exists then don't update the value for "price.item" if it also exists else set the value.
Some thing like $setOnInsert also doesn't meet the requirement I only need to update the value.
Thanks in advance.

Got the solution.
We can achieve this by first defining our update field something like:
let fieldName = `price.${itemNo}`
Then doing something like:
await Coll.findOneAndUpdate({
uuid
},
[{
$set: {
[fieldName]: {
$cond: [{
$or: [
{$ne: ["$price", null]},
{$ne: [`$${fieldName}`, null]}
]
}, valueToBeSet, `$${fieldName}`]
}
}
}], {upsert: true}

Related

How to return field based on other fields with mongoose

I have mongoose schema that looks something like this:
{
_id: someId,
name: 'mike',
keys: {
apiKey: 'fsddsfdsfdsffds',
secretKey: 'sddfsfdsfdsfdsds'
}
}
I don't want to send back to the front the keys of course, but I want some indication, like:
{
_id: someId,
name: 'mike',
hasKeys: true
}
There is built in way to create 'field' on the way based on other fields, or do I need every time fetch the whole document, check if keys is not empty and set object property based on that?
For Mongo version 4.2+ What you're looking for is called pipelined updates, it let's you use a (restricted) aggregate pipeline as your update allowing the usage of existing field values.
Here is a toy example with your data:
db.collection.updateOne(
{ _id: someId },
[
{
"$set": {
"hasKeys": {
$cond: [
{
$ifNull: [
"$keys",
false
]
},
true,
false
]
}
}
},
])
Mongo Playground
For older Mongo versions you have to do it in code.
If you don't want to update the actual document but just populate this field when you fetch it you can use the same aggregation to fetch the document
you can use $project in mongoose aggregation like this.
$project: { hasKeys: { $cond: [{ $eq: ['$keys', null] }, false, true]}}

Why does findOneAndUpdate not return document even though the update does happen?

I am encountering an issue with findOneAndUpdate operation on nodejs mongodb library, where I cannot tell if an update was done or not.
I wrote a query that can successfully update the document as expected, but value:null was returned after the update. Ultimately, I would like to distinguish between 3 cases:
Document found, cannot be updated
Document found and successfully updated
Document not found
const result = await collection.findOneAndUpdate(
{ caseId: caseId, list: {$ne: userId}, $expr: {$lt:[ {$size: '$list'}, 'maxLength']} },
{ $push: {assignees: userId}})
return result.value;
A little explaination on the query:
I want to append userId to the list field of a document if
caseId matches,
the user is not currently in the list,
the size of the list is smaller than the maxLength
Adding {new: true} will return updated document in result
const result = await collection.findOneAndUpdate(
{ caseId: caseId, list: {$ne: userId}, $expr: {$lt:[ {$size: '$list'}, 'maxLength']} },
{ $push: {assignees: userId}},
{new: true}
)
Adding a $set update operator solved my problem.
It seems that the $push update operator does not trigger an update notification to nodejs. The new element is appended to the collection, but the response treated it as no update done.
const result = await collection.findOneAndUpdate(
{ caseId: caseId, list: {$ne: userId}, $expr: {$lt:[ {$size: '$list'}, 'maxLength']} },
{ $push: {assignees: userId}, $set: {caseId: caseId}})
return result.value;

mongodb remove document if array count zero after $pull in a single query

I have a requirement where my comments schema looks like the following
{
"_id": 1,
"comments": [
{ "userId": "123", "comment": "nice" },
{ "userId": "124", "comment": "super"}
]
}
I would like to pull the elements based on the userId field.
I am doing the following query
comments.update({},{$pull:{comments:{userId:"123"}}})
My requirement is that if the array length became zero after the pull operator I need to remove the entire document for some reason.Is there a away to do this in a single query?
PS:I am using the mongodb driver.Not the mongoose
If I'm reading your question right, after the $pull, if the comments array is empty (zero length), then remove the document ({ _id: '', comments: [] }).
This should remove all documents where the comments array exists and is empty:
comments.remove({ comments: { $exists: true, $size: 0 } })
I had a similar requirement and used this (using mongoose though):
await Attributes.update({}, { $pull: { values: { id: { $in: valueIds } } } }, { multi: true })
await Attributes.remove({ values: { $exists: true, $size: 0 } })
Not sure if it's possible to do this in one operation or not.
You can use middlewares for this.
http://mongoosejs.com/docs/middleware.html
Write a pre/post update method in mongodb to check your condition.

Combining 2 queries into 1 query

Let's assume I have a structure as;
{
name: "",
surname: "",
id: 1,
children: [
{id: 2, name: "", age: ""},
{id: 3, name: "", age: ""}
]
}
I want to add new children or update the existing children fields according to situation.
To add new children into the list, I wrote query which is;
db.collection.update({id: 1}, {
$push: {
children: {
name: "alex",
age: 12,
id : shortid.generate()
}
}
}, {
upsert:true
},function(err, result) {
}
Whether it is a insert or update, I should update the document with id 1. So I think about combining these 2 situations into 1 query.
If I want to add children to the children array I should use $push, otherwise if it is an update I should update the fields of children object. So by using $cond I wrote a query like this;
db.collection.update({id: 1},
{
$cond : {
if: {id: {$exists: 21}} ,
then: {
$set: {
children: { name: "new name", age: "new age"}
}
},
else: {
$push: {
children: {
name: "alex",
age: 12,
id : 21
}
}
}
}, {
upsert:true
},function(err, result){}
And I see it is not possible to have such query, I got an error. Was it too unrealistic? Should I take this two situation separately? I think it was a good idea because I update the same document with id:1 whether it's a update or insert. The only change is deciding whether I will set the fields or create and push them into the children array. What do you suggest for this situation? And yes, I admit, I'm trying to get some experience with mongodb and $cond.
Your first solution is correct and does what you want it to do. The upsert option tells mongo to check if the document exists or not. Mongo will then either insert the doc if it's not there or update an existing doc with the new values it a doc with the given id exists. You need not to do anything else.

Mongoose, update values in array of objects

Is there a way to update values in an object?
{
_id: 1,
name: 'John Smith',
items: [{
id: 1,
name: 'item 1',
value: 'one'
},{
id: 2,
name: 'item 2',
value: 'two'
}]
}
Lets say I want to update the name and value items for item where id = 2;
I have tried the following w/ mongoose:
var update = {name: 'updated item2', value: 'two updated'};
Person.update({'items.id': 2}, {'$set': {'items.$': update}}, function(err) { ...
Problem with this approach is that it updates/sets the entire object, therefore in this case I lose the id field.
Is there a better way in mongoose to set certain values in an array but leave other values alone?
I have also queried for just the Person:
Person.find({...}, function(err, person) {
person.items ..... // I might be able to search through all the items here and find item with id 2 then update the values I want and call person.save().
});
You're close; you should use dot notation in your use of the $ update operator to do that:
Person.update({'items.id': 2}, {'$set': {
'items.$.name': 'updated item2',
'items.$.value': 'two updated'
}}, function(err) { ...
model.update(
{ _id: 1, "items.id": "2" },
{
$set: {
"items.$.name": "yourValue",
"items.$.value": "yourvalue",
}
}
)
MongoDB Document
There is a mongoose way for doing it.
const itemId = 2;
const query = {
item._id: itemId
};
Person.findOne(query).then(doc => {
item = doc.items.id(itemId );
item["name"] = "new name";
item["value"] = "new value";
doc.save();
//sent respnse to client
}).catch(err => {
console.log('Oh! Dark')
});
There is one thing to remember, when you are searching the object in array on the basis of more than one condition then use $elemMatch
Person.update(
{
_id: 5,
grades: { $elemMatch: { grade: { $lte: 90 }, mean: { $gt: 80 } } }
},
{ $set: { "grades.$.std" : 6 } }
)
here is the docs
For each document, the update operator $set can set multiple values, so rather than replacing the entire object in the items array, you can set the name and value fields of the object individually.
{'$set': {'items.$.name': update.name , 'items.$.value': update.value}}
Below is an example of how to update the value in the array of objects more dynamically.
Person.findOneAndUpdate({_id: id},
{
"$set": {[`items.$[outer].${propertyName}`]: value}
},
{
"arrayFilters": [{ "outer.id": itemId }]
},
function(err, response) {
...
})
Note that by doing it that way, you would be able to update even deeper levels of the nested array by adding additional arrayFilters and positional operator like so:
"$set": {[`items.$[outer].innerItems.$[inner].${propertyName}`]: value}
"arrayFilters":[{ "outer.id": itemId },{ "inner.id": innerItemId }]
More usage can be found in the official docs.
cleaner solution using findOneAndUpdate
await Person.findOneAndUpdate(
{ _id: id, 'items.id': 2 },
{
$set: {
'items.$.name': 'updated item2',
'items.$.value': 'two updated',
}
},
);
In Mongoose, we can update array value using $set inside dot(.) notation to specific value in following way
db.collection.update({"_id": args._id, "viewData._id": widgetId}, {$set: {"viewData.$.widgetData": widgetDoc.widgetData}})
Having tried other solutions which worked fine, but the pitfall of their answers is that only fields already existing would update adding upsert to it would do nothing, so I came up with this.
Person.update({'items.id': 2}, {$set: {
'items': { "item1", "item2", "item3", "item4" } }, {upsert:
true })
I had similar issues. Here is the cleanest way to do it.
const personQuery = {
_id: 1
}
const itemID = 2;
Person.findOne(personQuery).then(item => {
const audioIndex = item.items.map(item => item.id).indexOf(itemID);
item.items[audioIndex].name = 'Name value';
item.save();
});
Found this solution using dot-object and it helped me.
import dot from "dot-object";
const user = await User.findByIdAndUpdate(id, { ...dot.dot(req.body) });
I needed to update an array element with dynamic key-value pairs.
By mapping the update object to new keys containing the $ update operator, I am no longer bound to know the updated keys of the array element and instead assemble a new update object on the fly.
update = {
name: "Andy",
newKey: "new value"
}
new_update = Object.fromEntries(
Object.entries(update).map(
([k, v], i) => ["my_array.$." + k, v]
)
)
console.log({
"$set": new_update
})
In mongoose we can update, like simple array
user.updateInfoByIndex(0,"test")
User.methods.updateInfoByIndex = function(index, info) ={
this.arrayField[index]=info
this.save()
}
update(
{_id: 1, 'items.id': 2},
{'$set': {'items.$[]': update}},
{new: true})
Here is the doc about $[]: https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up.S[]

Resources