Using Mongoose / MongoDB $addToSet functionality on array of objects - node.js

say I have this array property ('articles') on a Mongoose schema:
articles: [
{
kind: 'bear',
hashtag: 'foo'
},
{
kind: 'llama',
hashtag: 'baz',
},
{
kind: 'sheep',
hashtag: 'bar',
}
]
how can I use
$addToSet https://docs.mongodb.org/manual/reference/operator/update/addToSet/
to add to this array by checking the value of hashtag to see if it's unique?
For example, if I want to add the following object to the above array, I want Mongo to 'reject' it as a duplicate:
{
kind: 'tortoise',
hashtag: 'foo'
}
because hashtag=foo has already been taken.
The problem is that I only know how to use $addToSet with simple arrays of integers...
for example, if articles looked like this:
articles: [ 1 , 5 , 4, 2]
I would use $addToSet like this:
var data = {
"$addToSet": {
"articles": 9
}
}
model.update(data);
but how can I accomplish the same thing with an array of objects where the unique field is a string, in this case 'hashtag'? The docs don't make this clear and it seems like I have searched everywhere..
thanks

You need to use the $ne operator.
var data = { 'kind': 'tortoise', 'hashtag': 'foo' };
Model.update(
{ 'articles.hashtag': { '$ne': 'foo' } },
{ '$addToSet': { 'articles': data } }
)
This will update the document only if there is no sub document in the "article" array with the value of hashtag equals to "foo".
As #BlakesSeven mentioned in the comment
The $addToSet becomes irrelevant once you are testing for the presence of one of the values, so this may as well be a $push for code clarity. But the principle is correct since $addToSet works on the whole object and not just part of it.
Model.update({
{ 'articles.hashtag': { '$ne': 'foo' } },
{ '$push': {'articles': data } }
)

// add the comment's id to the commentsList :
// share.comments.commentsList.addToSet(callback._id);
share.update(
{ '$push': {'comments.commentsList': mongoose.Types.ObjectId(callback._id) } }
, function(){
console.log('added comment id to the commentsList array of obectIds')
})

Related

Mondodb update array inside array (embeded) (mongoose/nodejs)

Hello i'm trying to update an embeded document inside an embeded document.
My "object" looks something like this
{
_id:0,
fieldOne:"f1",
fieldTwo : "f2",
subDocument:[
{
_id:0,
subfieldOne:"f1",
subfieldTwo:"f2",
subSubDocument:[
{
_id:0,
sub_subfieldOne:"f1",
sub_subfieldTwo:"f2"
}
]
},
],
}
afer some resarch i found out that you can update a sub sub array item if you know his position,
something like this
await Document.updateOne(
{ "subDocument._id": 0},
{ $set:{"subDocument.0.subsubDocument.0.sub_subfieldOne":"testaroo"} });
howerver if i loop through the arrays like
for(let i = 0;i<subDocument.length;i++){
for(let j = 0;j<subDocument[i].subSubDocument.length;j++){
await Document.updateOne(
{ "subDocument._id": 0},
{ $set:{"subDocument."+i+".subsubDocument."+j+".sub_subfieldOne":"testaroo"} });
}
}
he does not like the string concatenation in the query like this.....is there a way to do it?
After reading your code, I think you are trying to update sub_subfieldOne of all sub documents. If so, you can use $[] operator. It indicates that the update operator should modify all elements in the specified array field:
await Document.updateOne(
{ "subDocument._id": 0},
{ $set:{"subDocument.$[].subsubDocument.$[].sub_subfieldOne": "testaroo"} }
);

Conditional filtering against a nested object in MongoDB

I am having a problem searching for a key of a nested object.
I have search criteria object that may or may not have certain fields I'd like to search on.
The way I'm solving this is to use conditional statements to append to a "match criteria" object that gets passed to the aggregate $match operator. it works well until I need to match to something inside a nested object.
Here is a sample document structure
{
name: string,
dates: {
actived: Date,
suspended: Date
},
address : [{
street: string,
city: string,
state: string,
zip: string
}]
};
My criteria object is populated thru a UI and passed a JSON that looks similar to this:
{
"name": "",
"state": ""
}
And although I can explicitly use "dates.suspended" without issue -
when I try to append address.state to my search match criteria - I get an error.
module.exports.search = function( criteria, callback )
let matchCriteria = {
"name": criteria.name,
"dates.suspended": null
};
if ( criteria.state !== '' ) {
// *** PROBLEM HAPPENS HERE *** //
matchCriteria.address.state = criteria.state;
}
User.aggregate([
{ "$match": matchCriteria },
{ "$addFields": {...} },
{ "$project": {...} }
], callback );
}
I get the error:
TypeError: Cannot set property 'state' of undefined
I understand that I'm specifying 'address.state' when 'address' doesn't exist yet - but I am unclear what my syntax would be surely it woulnd't be matchCriteria['address.state'] or "matchCriteria.address.state"
Is there a better way to do conditional filtering?
For search in Nested Object, You have to use unwind
A query that help you :
//For testing declare criteria as const
let criteria = {name : 'name', 'state' : 'state'};
let addressMatch = {};
let matchCriteria = {
"name": criteria.name,
"dates.suspended": null
};
if ( criteria.state) {
addressMatch = { 'address.state' : criteria.state };
}
db.getCollection('user').aggregate([{
$match :matchCriteria,
},{$unwind:'$address'},
{$match : addressMatch}
])
Firstly check for address, and then access the property as shown:
if(matchCriteria['address']) {
matchCriteria['address']['state'] = criteria['state'];
}
else {
//otherwise
}
This should fix it:
matchCriteria['address.state'] = criteria.state;

mongodb $pull doesn't work with array in subdocument

I have a problem with an update with MongoDB.
My schema look like this:
Project: {
_id: ObjectId(pro_id)
// some data
dashboard_group: [
{
_id: ObjectId(dgr_id)
dgr_name: "My Dashboard"
dgr_tasks: [
id1,
id2,
...
]
},
// other dashboards
]
}
I want to remove id2 but the $pull operator seems not work. Mongo return me this :
result: {
lastErrorObject: {
n: 1,
updatedExisting: true
},
ok: 1
}
This is my request:
db.Project.findOneAndUpdate({
"dashboard_group._id": dgr_id
}, {
$pull: {
"dashboard_group.$.dgr_tasks": id2
}
});
dgr_id is already cast to ObjectId before the query and I verified the value that I want to remove.
Can anyone have an idea ?
You will need to select the particular array element using "$elemMatch" like this
Query : {"dashboard_group":{"$elemMatch":{dgr_name:"My Dashboard"}}}
Update : {$pull:{"dashboard_group.$.dgr_tasks":"id2"}}
So, I found a solution with the $[] identifier. It's not its basic utility, but it fit to my case.
A task ID cannot be at 2 location, it belongs to 1 and only 1 dashboard. So if you make a request like :
db.Project.findOneAndModify({
"dashboard_group._id": dgr_id
}, {
$pull: {
"dashboard_group.$[].dgr_tasks": id2
}
});
Mongo will remove all value that match id2. Without the {multi: true} option, it will make the update 1 time, and my item is indeed remove from my nested array.

how to remove object in array by index mongodb / mongoose [duplicate]

In the following example, assume the document is in the db.people collection.
How to remove the 3rd element of the interests array by it's index?
{
"_id" : ObjectId("4d1cb5de451600000000497a"),
"name" : "dannie",
"interests" : [
"guitar",
"programming",
"gadgets",
"reading"
]
}
This is my current solution:
var interests = db.people.findOne({"name":"dannie"}).interests;
interests.splice(2,1)
db.people.update({"name":"dannie"}, {"$set" : {"interests" : interests}});
Is there a more direct way?
There is no straight way of pulling/removing by array index. In fact, this is an open issue http://jira.mongodb.org/browse/SERVER-1014 , you may vote for it.
The workaround is using $unset and then $pull:
db.lists.update({}, {$unset : {"interests.3" : 1 }})
db.lists.update({}, {$pull : {"interests" : null}})
Update: as mentioned in some of the comments this approach is not atomic and can cause some race conditions if other clients read and/or write between the two operations. If we need the operation to be atomic, we could:
Read the document from the database
Update the document and remove the item in the array
Replace the document in the database. To ensure the document has not changed since we read it, we can use the update if current pattern described in the mongo docs
You can use $pull modifier of update operation for removing a particular element in an array. In case you provided a query will look like this:
db.people.update({"name":"dannie"}, {'$pull': {"interests": "guitar"}})
Also, you may consider using $pullAll for removing all occurrences. More about this on the official documentation page - http://www.mongodb.org/display/DOCS/Updating#Updating-%24pull
This doesn't use index as a criteria for removing an element, but still might help in cases similar to yours. IMO, using indexes for addressing elements inside an array is not very reliable since mongodb isn't consistent on an elements order as fas as I know.
in Mongodb 4.2 you can do this:
db.example.update({}, [
{$set: {field: {
$concatArrays: [
{$slice: ["$field", P]},
{$slice: ["$field", {$add: [1, P]}, {$size: "$field"}]}
]
}}}
]);
P is the index of element you want to remove from array.
If you want to remove from P till end:
db.example.update({}, [
{ $set: { field: { $slice: ["$field", 1] } } },
]);
Starting in Mongo 4.4, the $function aggregation operator allows applying a custom javascript function to implement behaviour not supported by the MongoDB Query Language.
For instance, in order to update an array by removing an element at a given index:
// { "name": "dannie", "interests": ["guitar", "programming", "gadgets", "reading"] }
db.collection.update(
{ "name": "dannie" },
[{ $set:
{ "interests":
{ $function: {
body: function(interests) { interests.splice(2, 1); return interests; },
args: ["$interests"],
lang: "js"
}}
}
}]
)
// { "name": "dannie", "interests": ["guitar", "programming", "reading"] }
$function takes 3 parameters:
body, which is the function to apply, whose parameter is the array to modify. The function here simply consists in using splice to remove 1 element at index 2.
args, which contains the fields from the record that the body function takes as parameter. In our case "$interests".
lang, which is the language in which the body function is written. Only js is currently available.
Rather than using the unset (as in the accepted answer), I solve this by setting the field to a unique value (i.e. not NULL) and then immediately pulling that value. A little safer from an asynch perspective. Here is the code:
var update = {};
var key = "ToBePulled_"+ new Date().toString();
update['feedback.'+index] = key;
Venues.update(venueId, {$set: update});
return Venues.update(venueId, {$pull: {feedback: key}});
Hopefully mongo will address this, perhaps by extending the $position modifier to support $pull as well as $push.
I would recommend using a GUID (I tend to use ObjectID) field, or an auto-incrementing field for each sub-document in the array.
With this GUID it is easy to issue a $pull and be sure that the correct one will be pulled. Same goes for other array operations.
For people who are searching an answer using mongoose with nodejs. This is how I do it.
exports.deletePregunta = function (req, res) {
let codTest = req.params.tCodigo;
let indexPregunta = req.body.pregunta; // the index that come from frontend
let inPregunta = `tPreguntas.0.pregunta.${indexPregunta}`; // my field in my db
let inOpciones = `tPreguntas.0.opciones.${indexPregunta}`; // my other field in my db
let inTipo = `tPreguntas.0.tipo.${indexPregunta}`; // my other field in my db
Test.findOneAndUpdate({ tCodigo: codTest },
{
'$unset': {
[inPregunta]: 1, // put the field with []
[inOpciones]: 1,
[inTipo]: 1
}
}).then(()=>{
Test.findOneAndUpdate({ tCodigo: codTest }, {
'$pull': {
'tPreguntas.0.pregunta': null,
'tPreguntas.0.opciones': null,
'tPreguntas.0.tipo': null
}
}).then(testModificado => {
if (!testModificado) {
res.status(404).send({ accion: 'deletePregunta', message: 'No se ha podido borrar esa pregunta ' });
} else {
res.status(200).send({ accion: 'deletePregunta', message: 'Pregunta borrada correctamente' });
}
})}).catch(err => { res.status(500).send({ accion: 'deletePregunta', message: 'error en la base de datos ' + err }); });
}
I can rewrite this answer if it dont understand very well, but I think is okay.
Hope this help you, I lost a lot of time facing this issue.
It is little bit late but some may find it useful who are using robo3t-
db.getCollection('people').update(
{"name":"dannie"},
{ $pull:
{
interests: "guitar" // you can change value to
}
},
{ multi: true }
);
If you have values something like -
property: [
{
"key" : "key1",
"value" : "value 1"
},
{
"key" : "key2",
"value" : "value 2"
},
{
"key" : "key3",
"value" : "value 3"
}
]
and you want to delete a record where the key is key3 then you can use something -
db.getCollection('people').update(
{"name":"dannie"},
{ $pull:
{
property: { key: "key3"} // you can change value to
}
},
{ multi: true }
);
The same goes for the nested property.
this can be done using $pop operator,
db.getCollection('collection_name').updateOne( {}, {$pop: {"path_to_array_object":1}})

$addToSet aggregation and multiple arrays

I have this collection :
[
{
_id: ObjectId('myId1'),
probes: ['id_probe_1', 'id_probe_2']
},
{
_id: ObjectId('myId2'),
probes: ['id_probe_1', 'id_probe_3']
}
]
I want to get an array like this :
['id_probe_1', 'id_probe_2', 'id_probe_3']
So I try this request (from nodeJS driver) :
let find = [
{
$match: {
_id: {
$in: [new ObjectId('miId1'), new ObjectId('myId2')]
}
}
},
{
$group: {
_id: null,
probes: {
$addToSet: {
$each: '$probes'
}
}
}
}
];
This doesn't work, give me this error :
invalid operator '$each'
From the doc, they mention that it will appends the whole array as a single element.
If the value of the expression is an array, $addToSet appends the whole array as a single element.
But they don't say how to have an unique array. So I use the $each operator like this page indicates (I don't really know what's the difference...)
Is there a way to make this work ?
Thanks !
insert $unwind before $group
{$unwind:"$probes"},
then remove $each
Why don't you try distinct operation? In mongo shell, db.col.distinct('probs'); you can try the distinct function in nodejs mongo driver.

Resources