mongoose #set and $unset conditional - node.js

in my project i want to allow optional fields in a document, but i don't want to save any null values, i.e. i want to remove any field with null.
the values can be changed during run time.
i found how i can make a field null if the user didn't send data to update it (all the fields that the user send empty values for should get deleted).
if the user send firstName/lastName as an empty string in the form i want to remove them.
await Employee.findOneAndUpdate({ _id: employee._id },
{$set: {firstName: null, lastName: null, ...req.body.newEmployee}}, { new: true, upsert: false });
i tried to add an $unset option but i got an error
MongoError: Updating the path 'firstName' would create a conflict at 'firstName'
i thought about deleting it after the update (as a second command) but i can't find a way to tell/get all the null fields of the employee, and i can't get a fixed values to check because there are many combination for the null/not null values, especially if there will be more fields in the future.
i.e. i cant tell if (FN: null, LN: null or FN: "johny", LN: null etc..)

Update : In case if you need to keep some fields as is in existing document then try this, with this new code only the fields coming in request will either be updated or deleted doesn't alter other existing fields in document :
Your node.js code :
let removeObj = {}
Object.entries(req.body).forEach(([key, val]) => {
if (!val) { delete req[body][key]; removeObj[key] = '' };
})
let bulkArr = []
if (req.body) bulkArr.push({
updateOne: {
"filter": { "_id": ObjectId("5e02da86400289966e8ffa4f") },
"update": { $set: req.body }
}
})
if (Object.entries(removeObj).length > 0 && removeObj.constructor === Object) bulkArr.push({
updateOne: {
"filter": { "_id": ObjectId("5e02da86400289966e8ffa4f") },
"update": { $unset: removeObj }
}
})
// For mongoDB4.2 removeObj has to be an array of strings(field names)
let removeObj = []
Object.entries(req.body).forEach(([key, val]) => {
if (!val) { delete req[body][key]; removeObj.push(key) };
})
// After this, Similar to above you need to write code to exclude empty removeObj for 4.2 as well.
Query :: On mongoDB version >= 3.2 to < 4.2 .bulkWrite():
db.yourCollectionName.bulkWrite(bulkArr)
Query :: From mongoDB version 4.2 .updateOne accepts aggregation pipeline :
db.yourCollectionName.updateOne(
{ "_id": ObjectId("5e02da86400289966e8ffa4f") },
[
{
$set: req.body
},
{ $unset: removeObj }
]
)
Old Answer : You need to try .findOneAndReplace() , if you want entire doc to be replaced :
db.yourCollectionName.findOneAndReplace({_id: ObjectId("5e02da86400289966e8ffa4f")},inputObj, {returnNewDocument:true})
Your collection :
{
"_id" : ObjectId("5e02da86400289966e8ffa4f"),
"firstName" : "noName",
"lastName": 'noName',
"value" : 1.0,
"empDept": 'general',
"password":'something'
}
Your request object is like :
req.body = {
firstName: 'firstName',
lastName: null,
value : 1.0,
empDept: ''
}
your node.js code (Removes all falsy values ( "", 0, false, null, undefined )) :
let inputObj = Object.entries(req.body).reduce((a,[k,v]) => (v ? {...a, [k]:v} : a), {})
your collection after operation :
{
"_id" : ObjectId("5e02da86400289966e8ffa4f"),
"firstName" : "firstName",
"value" : 1.0,
}
your collection after operation as per updated answer :
{
"_id" : ObjectId("5e02da86400289966e8ffa4f"),
"firstName" : "firstName",
"value" : 1.0,
"password":'something'
}

Related

Mongoose/Node.js : The positional operator did not find the match needed from the query

I try to update a MongoDB collection with Mongoose & NodeJS but I have the following error who appears sometimes, but not all the times :
MongoServerError: Plan executor error during findAndModify :: caused by :: The positional operator did not find the match needed from the query.
Here is my code:
NodeJS
try {
this.myArrayUpdate = { 'description': 'test' };
this.update = { $set:
{ 'myArray.$': this.myArrayUpdate }
};
const cardId = ObjectId("61d6e320520fac65775a7ba8")
const userId = ObjectId("61a8e433648e1963ae7358be");
const filter = {
userId: userId,
'myArray._id': cardId,
};
const options = {
upsert: true,
new: true
};
MyCollection
.findOneAndUpdate(
filter,
this.update,
options,
)
.then ( () => {
return res.status(204).json({ message: 'MyCollection updated ' });
});
} catch (error) {
throw error;
}
Mongodb
{
"_id" : ObjectId("61a8e433648e1963ae7358be"),
"myArray" : [
{
"description" : "test",
"starting" : null,
"ending" : null,
"_id" : ObjectId("61d6e320520fac65775a7ba8")
},
{
"description" : "test 2",
"starting" : null,
"ending" : null,
"_id" : ObjectId("61d6e320520fac657812991a")
},
],
}
If found a part of an answer, each time I update my subdocument array, the _id change so it's why there is an error, if I update my page no more error !
Is there a possibility to not create a new _id when updating a subdocument array ?
Thanks for your answer,
David
The document you provided doesn't have an userId field, so your filter won't match the document. The field is called _id:
const filter = {
_id: userId,
'myArray._id': cardId,
};
Also, the way the update document is written, the entire matched element in myArray will be overwritten, but I assume you only want to update description. It would look like this:
this.update = { $set:
{ 'myArray.$.description': 'test' }
};
This is an example of updating a document in an array. Here's a working example in Mongo playground.

Prevent mongoose "Model.updateOne" from updating ObjectId(_id) of the model when using "$set"

I'm updating the age and name of a character with a specific _id from an array of characters that is inside a document of model Drama.
The document I'm working with:-
{
"_id" : ObjectId("619d44d2ec2ca20ca0404b5a"),
"characters" : [
{
"_id" : ObjectId("619fdac5a03c8b10d0b8b13c"),
"age" : "23",
"name" : "Vinay",
},
{
"_id" : ObjectId("619fe1d53810a130207a409d"),
"age" : "25",
"name" : "Raghu",
},
{
"_id" : ObjectId("619fe1d53810a130207a502v"),
"age" : "27",
"name" : "Teju",
}
],
}
So to update the character Raghu I did this:-
const characterObj = {
age: "26",
name: "Dr. Raghu",
};
Drama.updateOne(
{ _id: req.drama._id, "characters._id": characterId },
{
$set: {
"characters.$": characterObj,
},
},
function(err, foundlist) {
if (err) {
console.log(err);
} else {
console.log("Update completed");
}
}
);
// req.drama._id is ObjectId("619d44d2ec2ca20ca0404b5a")
// characterId is ObjectId("619fe1d53810a130207a409d")
This updated the character but it also assigned a new ObjectId to the _id field of the character. So, I'm looking for ways on how to prevent the _id update.
Also, I know I can set the individual fields of character instead of assigning a whole new object to prevent that but it will be very tedious if my character's object has a lot of fields.
//Not looking to do it this way
$set: {
"characters.$.age": characterObj.age,
"characters.$.name": characterObj.name,
},
Thanks.
I found something here, just pre define a schema (a blueprint in a way) that affects the id
var subSchema = mongoose.Schema({
//your subschema content
},{ _id : false });
Stop Mongoose from creating _id property for sub-document array items
Or I would say, when you create a character assign it a custom id from the start, that way it will retain that id throughout.
I'm leaving this question open as I would still like to see a simpler approach. But for now, I did find one easy alternative solution for this issue which I'm will be using for some time now until I find a more direct approach.
In short - Deep merge the new object in the old object using lodash and then use the new merged object to set field value.
For example, let's update the character Raghu from my question document:-
First install lodash(Required for deep merging objects) using npm:
$ npm i -g npm
$ npm i --save lodash
Import lodash:
const _ = require("lodash");
Now update the character Raghu like this:-
const newCharacterObj = {
age: "26",
name: "Dr. Raghu",
};
Drama.findById(
{ _id: req.drama._id, "characters._id": characterId },
"characters.$",
function(err, dramaDocWithSpecificCharacter) {
console.log(dramaDocWithSpecificCharacter);
// ↓↓↓ console would log ↓↓↓
// {
// "_id" : ObjectId("619d44d2ec2ca20ca0404b5a"),
// "characters" : [
// {
// "_id" : ObjectId("619fe1d53810a130207a409d"),
// "age" : "25",
// "name" : "Raghu",
// }
// ],
// }
const oldCharacterObj = dramaDocWithSpecificCharacter.characters[0];
const mergedCharacterObjs = _.merge(oldCharacterObj, newCharacterObj);
// _.merge() returns a deep merged object
console.log(mergedCharacterObjs);
// ↓↓↓ console would log ↓↓↓
// {
// _id: 619fe1d53810a130207a409d,
// age: "26",
// name: "Dr. Raghu",
// };
Drama.updateOne(
{ _id: req.drama._id, "characters._id": characterId },
{
$set: {
"characters.$": mergedCharacterObjs,
},
},
function(err, foundlist) {
if (err) {
console.log(err);
} else {
console.log("Update completed");
}
}
);
}
);
// req.drama._id is ObjectId("619d44d2ec2ca20ca0404b5a")
// characterId is ObjectId("619fe1d53810a130207a409d")
Note: We can also use the native Object.assign() or … (spread operator) to merge objects but the downside of it is that it doesn’t merge nested objects which could cause issues if you later decide to add nested objects without making changes for deep merge.
You can pass your payload or request body like this if we provide _id it will prevent update to nested document
"characters" : [
{
"_id" : "619fdac5a03c8b10d0b8b13c",
"age" : "updated value",
"name" : "updated value",
}, {
"_id" : "619fe1d53810a130207a409d",
"age" : "updated value",
"name" : "updated value",
}, {
"_id" : "619fe1d53810a130207a502v",
"age" : "updated value",
"name" : "updated value",
}
],
It works for me for bulk update in array object

MongoDB add field to an object inside an array

I have an object user that looks like that
{
"_id" : ObjectId("5edbdf57ac52325464b054ec"),
...
"purchaseHistory" : [
{
"_id" : ObjectId("5ee7a8f6b438a1254cec3f74"),
...
},
{
"_id" : ObjectId("5ee7a8f6b438a1254cec3f88"),
...
}
]
}
What I wanna do is to add a new field to a specific object inside "purchaseHistory" by ID, for example I wanna add to "5ee7a8f6b438a1254cec3f88" a field "status": 0
What I tried is
users.findOneAndUpdate(
{
_id: ObjectId(userId),
'purchaseHistory._id': ObjectId(saleId)
},
{
$set: { 'purchaseHistory.$.status': status}
}
)
But it gives me an error, how can I do it properly?
According to the website provided by D. SM, I was able to do it this way
users.findOneAndUpdate(
{
'_id': ObjectId(userId),
'purchaseHistory._id': ObjectId(saleId)
},
{
$set: { 'purchaseHistory.$.status': status }
}
)
MongoDB provides the positional update operator for cases like this.

How to compare array mongodb query

I am currently building a cart system on my mongodb ecommerce app. I need help on how to query and compare array.
here document of cart:
{
"_id" : ObjectId("5d0531e27c8fa1029017ea20"),
"user" : ObjectId("5d0371319315c715fc34b0b0"),
"active" : true,
"item" : [
{
"product" : ObjectId("5d013eb63a2bdd11a46c8dd3"),
"option" : [
{
"name" : "Ukuran",
"value" : "Biru"
}
],
"quantity" : 1
},
{
"product" : ObjectId("5d013eb63a2bdd11a46c8dd3"),
"option" : [
{
"name" : "Ukuran",
"value" : "Biru"
}
],
"quantity" : 1
}
],
"created_at" : ISODate("2019-06-15T17:58:58.762Z"),
"updated_at" : ISODate("2019-06-15T17:59:13.334Z"),
"__v" : 0
}
I want to compare object of item.option field, so my cart system is if cart on database have same object option i will add quantity, otherwise push new object to item.
so current I am not asking on how to implement my cart system, but I want to compare each item.option object
I've already tried this
const cart = await CartModel.find({
"item.option": option
})
and get error Error: Query filter must be an object, got an array
Solved by myself, after many experiment finally i combine $in and $elemMatch for compare each array of object
// this is will dynamic
const optionArray = [
{
"name": "Warna",
"value": "Biru"
},
{
"name": "Ukuran",
"value": "XL"
}
]
const compareOptionQuery = []
for (let i = 0; i < optionArray.length; i++) {
compareOptionQuery.push({
$elemMatch: {
...option[i]
}
})
}
const cart = await CartModel.aggregate([
{
$and: [
{
_id: cartId,
user: userId
},
{
'item.option': {
$in: [...compareOptionQuery]
}
}
]
}
])
The issue with your implementation is that in CartModel.find("item.option": option) your filter (first parameter) encounters an array instead of an object.
This is because you are trying to call for an object option from item, however option is as element of an array field item.
If you wish to access item from an array field, you must specify conditions on the elements in the array field item using {<array field>: {<operator1>: <value1>}} like so:
CartModel.find({
item: {$option: option}
})

How to get the max with a where condition in this situation Mongoose / Node

I am working with Mongoose, I have a collection that has documents like this
{
"_id" : 1,
"body" : "[{\"order_id\":\"647936\",\"order_datetime\":\"2015-12-02 11:10:00\"}]",
"user_info" : {
"contact_email" : "test#test.com",
"contact_phone" : "1234567",
},
"type" : "ORDERS",
"version" : 1
}
{
"_id" : 2,
"body" : "[{\"order_id\":\"647936\",\"order_datetime\":\"2015-12-02 11:10:00\"}]",
"user_info" : {
"contact_email" : "test#test.com",
"contact_phone" : "1234567",
},
"type" : "ORDERS",
"version" : 2
}
{
"_id" : 3,
"body" : "[{\"order_id\":\"647936\",\"order_datetime\":\"2015-12-02 11:10:00\"}]",
"user_info" : {
"contact_email" : "test#test.com",
"contact_phone" : "1234567",
},
"type" : "ORDERS",
"version" : 3
}
As you can see in body field you can see the order_id , so same order_id can be repeated in multiple in documents but the version will be different.
What I want is I want to search for the maximum version number for a
given order_id .
In my case it would be 3 .
I tried to use simple queries like
myCollection.aggregate([
{ "$match" : { "body.order_id" : 647936 } },
{ "$group": {
"_id" :"version",
"max": { "$max": "version" }
}}
] , function(err, data){
console.log(err);
console.log(data);
});
But the result is
null
[]
** Note that my mongoose connection is working fine and I can do some simple queries and results are OK.
Your data is the problem here since what seems to be intended as a structured document has been stored as a string:
// Bad bit
"body" : "[{\"order_id\":\"647936\",\"order_datetime\":\"2015-12-02 11:10:00\"}]",
Instead you would want this:
// Acutally data and not a string
"body" : [{ "order_id": "647936", "order_datetime": ISODate("2015-12-02 11:10:00.000Z" }],
With data like that, getting the latest version is a simple matter of ordering the results, without the overhead of .aggregate():
myCollection.find({ "body.order_id": "647936" })
.sort({ "version": -1 }).limit(1).exec(function(err,result) {
})
No need to aggregate and it's much faster than doing so, as you are just picking out the document with the latest (largest) version number.
In order to "fix" the data you can do something like this as a "one shot" execution in the shell:
var bulk = db.myCollection.initializeOrderedBulkOp(),
count = 0;
// query selects just those "body" elements that are currently a string
db.myCollection.find({ "body": { "$type": 2 } }).forEach(function(doc) {
var fixBody = JSON.parse(doc.body); // Just parse the string
fixBody.forEach(function(el) {
// Fix dates
el.order_datetime = new Date(
Date.parse(el.order_datetime.split(" ").join("T") + "Z")
);
});
// And queue an update to overwrite the "body" data
bulk.find({ "_id": doc._id }).updateOne({
"$set": { "body": fixBody }
});
count++;
// Send every 1000
if ( count % 1000 == 0 ) {
bulk.execute();
bulk = db.myCollection.initializeOrderedBulkOp(),
}
});
// Send any remaining batched
if ( count % 1000 != 0 )
bulk.execute();
You might also want to convert those "strings" other than the date to numeric values in a similar fashion and change appropriately in the query.

Resources