Update element in array with mongoose - mongodb

I have an array with multiple objects in it (there can be more than two). Now I need to update an element (change verified to true) inside the object (e.g. an object with method = "app"). If the object doesn't exist yet, it should be recreated. Is there any way to handle this with Mongoose?
I have found a solution for updating, but it does not solve the problem when no object exists
const result = await User.updateOne({email},
{ $set: { "multifactors.$[elem].verified" : true } },
{ arrayFilters: [ { "elem.method": "app" } ] }
)

This is one way of doing it, using a pipelined update.
a. Check if the array contains the required object using $filter and $size.
b. If it's there, updates the matching object $map.
c. Else, append the new object to the array.
db.collection.update({},
[
{
"$set": {
"multifactors": {
$cond: {
if: {
$gt: [
{
$size: {
$filter: {
input: "$multifactors",
as: "factor",
cond: {
$eq: [
"$$factor.method",
"app"
]
}
}
}
},
0
]
},
then: {
$map: {
input: "$multifactors",
as: "factor",
in: {
$cond: {
if: {
$eq: [
"$$factor.method",
"app"
]
},
then: {
$mergeObjects: [
"$$factor",
{
verified: true
}
]
},
else: "$$factor"
}
}
}
},
else: {
$concatArrays: [
"$multifactors",
[
{
method: "app",
verified: true
}
]
]
}
}
}
}
}
])
Playground link.

Related

Update or Insert object in array in MongoDB

I have the following collection
{
"_id" : ObjectId("57315ba4846dd82425ca2408"),
"myarray" : [
{
userId : "8bc32153-2bea-4dd5-8487-3b65e3aa0869",
Time:2022-09-20T04:44:46.000+00:00,
point : 5
},
{
userId : "5020db46-3b99-4c2d-8637-921d6abe8b26",
Time:2022-09-20T04:44:49.000+00:00
point : 2
},
]
}
These are my questions
I want to push into myarray if userId doesn’t exist, and if userid already exists then update time and point also I have to keep only 5 elements in the array if 6th element comes then I a have to sort the array based on Time and remove oldest time entry
what is the best way to do this in mongo using aggregation
FYI we are using Mongo 4.4
You can achieve this by using the aggregation pipeline update syntax, the strategy will be first to update the array (or insert a new element to it).
then if the size exceeds 5 we just filter it based on minimum value. like so:
const userObj = {point: 5, userId: "12345"};
db.collection.updateOne(
{ ...updateCondition },
[
{
$set: {
myarray: {
$cond: [
{
$in: [
userObj.userId,
{$ifNull: ["$myarray.userId", []]}
]
},
{
$map: {
input: "$myarray",
in: {
$cond: [
{
$eq: [
"$$this.userId",
userObj.userId
]
},
{
$mergeObjects: [
"$$this",
{
Time: "$$NOW", // i used "now" time but you can swap this to your input
point: userObj.point
}
]
},
"$$this"
]
}
}
},
{
$concatArrays: [
{ $ifNull: ["$myarray", []] },
[
{
userId: userObj.userId,
point: userObj.point,
Time: "$$NOW"
}
]
]
}
]
}
}
},
{
$set: {
myarray: {
$cond: [
{
$gt: [
{
$size: "$myarray"
},
5
]
},
{
$filter: {
input: "$myarray",
cond: {
$ne: [
"$$this.Time",
{
$min: "$myarray.Time"
}
]
}
}
},
"$myarray"
]
}
}
}
])
Mongo Playground

How to remove a field of an array's nested object that has an empty string value using mongodb aggregation?

So far, after i tried, i came up with solution where i am able to remove the whole object inside of the array if that object has field with empty value. That does not work in my case. I only need to remove the field and keep rest of the object. In this case, "Comment" field is the one having empty values occasionally. Thanks in advance!
Structure:
someArray: [
{
field1:"value",
field2:"value",
Comment:"",
Answer:"",
},
{
field1:"value",
field2:"value",
Comment:"",
Answer:"",
}]
Code:
$project: {
someArray: {
$filter: {
input: "$someArray", as: "array",
cond: { $ne: [ "$$array.Comment", ""]}}}}
Use $map to loop over the array elements.For each array element where comment is not an empty string, return whole element, otherwise return the document excluding comment field. Like this:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
as: "element",
in: {
$cond: {
if: {
$eq: [
"",
"$$element.Comment"
]
},
then: {
field1: "$$element.field1",
field2: "$$element.field2"
},
else: "$$element"
}
}
}
}
}
},
])
Here, is the working link.
Here is a solution where an array's nested object can have multiple fields and these need not be referred in the aggregation. Removes the nested object's field with value as an empty string (""):
db.collection.aggregate([
{
$set: {
someArray: {
$map: {
input: '$someArray',
as: 'e',
in: {
$let: {
vars: {
temp_var: {
$filter: {
input: { $objectToArray: '$$e' },
cond: { $ne: [ '', '$$this.v' ] },
}
}
},
in: {
$arrayToObject: '$$temp_var'
}
}
}
}
}
}
},
])
Solution from Charchit Kapoor works only if your array has exactly
{
field1: ...
field2: ...
Comment:""
}
But it does not work for arbitrary fields. I was looking for more generic solution, my first idea was this:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
in: {
$cond: {
if: { $eq: ["$$this.Comment", ""] },
then: { $mergeObjects: ["$$this", { Comment: "$$REMOVE" }] },
else: "$$this"
}
}
}
}
}
}
])
but it does not work.
I ended on this one:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
in: {
$cond: {
if: { $eq: ["", "$$this.Comment"] },
then: {
$arrayToObject: {
$filter: {
input: {
$map: {
input: { $objectToArray: "$$this" },
as: "element",
in: { $cond: [{ $eq: ["$$element.k", "Comment"] }, null, "$$element"] }
}
},
as: "filter",
cond: "$$filter" // removes null's from array
}
}
},
else: "$$this"
}
}
}
}
}
}
])
Mongo Playground

$filter inside $reduce or inside $map from array without unwind

I need some help:
I want to optimize this query to be faster , it need to filter by events.eventType:"log" all docs with server:"strong" , but without separate unwind & filter stages , maybe somehow inside the $reduce stage to add $filter.
example single document:
{
server: "strong",
events: [
{
eventType: "log",
createdAt: "2022-01-23T10:26:11.214Z",
visitorInfo: {
visitorId: "JohnID"
}
}
current aggregation query:
db.collection.aggregate([
{
$match: {
server: "strong"
}
},
{
$project: {
total: {
$reduce: {
input: "$events",
initialValue: {
visitor: [],
uniquevisitor: []
},
in: {
visitor: {
$concatArrays: [
"$$value.visitor",
[
"$$this.visitorInfo.visitorId"
]
]
},
uniquevisitor: {
$cond: [
{
$in: [
"$$this.visitorInfo.visitorId",
"$$value.uniquevisitor"
]
},
"$$value.uniquevisitor",
{
$concatArrays: [
"$$value.uniquevisitor",
[
"$$this.visitorInfo.visitorId"
]
]
}
]
}
}
}
}
}
}
])
expected output , two lists with unique visitorId & list of all visitorId:
[
{
"total": {
"uniquevisitor": [
"JohnID"
],
"visitor": [
"JohnID",
"JohnID"
]
}
}
]
playground
In the example query no filter is added for events.eventType:"log" , how can this be implemented without $unwind?
I am not sure this approach is more optimized than yours but might be this will help,
$filter to iterate loop of events and filter by eventType
$let to declare a variable events and store the above filters result
return array of visitor by using dot notation $$events.visitorInfo.visitorId
return array of unique visitor uniquevisitor by using dot notation $$events.visitorInfo.visitorId and $setUnion operator
db.collection.aggregate([
{ $match: { server: "strong" } },
{
$project: {
total: {
$let: {
vars: {
events: {
$filter: {
input: "$events",
cond: { $eq: ["$$this.eventType", "log"] }
}
}
},
in: {
visitor: "$$events.visitorInfo.visitorId",
uniquevisitor: {
$setUnion: "$$events.visitorInfo.visitorId"
}
}
}
}
}
}
])
Playground
Or similar approach without $let and two $project stages,
db.collection.aggregate([
{ $match: { server: "strong" } },
{
$project: {
events: {
$filter: {
input: "$events",
cond: { $eq: ["$$this.eventType", "log"] }
}
}
}
},
{
$project: {
total: {
visitor: "$events.visitorInfo.visitorId",
uniquevisitor: {
$setUnion: "$events.visitorInfo.visitorId"
}
}
}
}
])
Playground

MongoDB get all documents where array values match

I have the following structure of document:
{
"input": {
"fields": [
{
"name": "last_name_hebrew",
"text": "test1",
},
],
},
"output": {
"fields": [
{
"name": "last_name_hebrew",
"text": "test1"
},
],
},
},
I want to get all documents, where fields has object that has name of value last_name_hebrew as with text value of the output.fields.
For example in the given structure it would return this documents because input.fields.name is last_name_hebrew and text is equal to the text in output.
Note I cannot guarantee that fields array in either input or output will have name: last_name_hebrew in the array.
How can I do so?
This is my try to first force the arrays to have document with name of last_name_hebrew:
db.collection.find({
"input.fields": {
$elemMatch: {
"name": "last_name_hebrew"
}
},
"output.fields": {
$elemMatch: {
"name": "last_name_hebrew"
}
},
})
But now I need to compare the text values.
Your first 2 condition with $elemMatch is correct
add expression match, first find the matching element that having last_name_hebrew name from input using $filter and get first element from that filtered result using $arrayElemAt, same process for output field and then match both object using $eq
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$eq: [
{
$arrayElemAt: [
{
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
},
{
$arrayElemAt: [
{
$filter: {
input: "$output.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
]
}
});
Playground
Second option: if you want to go with more specific to match exact 2 fields name and text both just need to add $let operator to return fields from filter,
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$eq: [
{
$let: {
vars: {
input: {
$arrayElemAt: [
{
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
},
in: { name: "$$input.name", text: "$$input.text" }
}
},
{
$let: {
vars: {
output: {
$arrayElemAt: [
{
$filter: {
input: "$output.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
},
in: { name: "$$output.name", text: "$$output.text" }
}
}
]
}
})
Playground
Third option: for more specific to check both fields in loop,
first filter the matching elements by name in input field using $filter
pass above filter result in another filter
filter to match name and text field in output field, if its not [] empty then return filter result
$ne to check return result is not [] empty
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$ne: [
{
$filter: {
input: {
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
as: "i",
cond: {
$ne: [
{
$filter: {
input: "$output.fields",
cond: {
$and: [
{ $eq: ["$$this.name", "$$i.name"] },
{ $eq: ["$$this.text", "$$i.text"] }
]
}
}
},
[]
]
}
}
},
[]
]
}
})
Playground
You will have to use an aggregation pipeline to achieve this, there are several ways to do so, here is one example:
db.collection.aggregate([
{
$match: {
$expr: {
$gt: [
{
$size: {
$filter: {
input: "$input.fields",
as: "inputField",
cond: {
$and: [
{
$eq: [
"$$inputField.name",
"last_name_hebrew"
]
},
{
"$setIsSubset": [
[
"$$inputField.text"
],
"$output.fields.text"
]
}
]
}
}
}
},
0
]
}
}
}
])
Mongo Playground
One thing to note is that with this query there are no restrictions on the output.fields.name (as it was not required), if you do require the names to match then you can drop the .text field in the $setIsSubset operator.

conditional addFields to embedded objects in MongoDb

I am trying to add in values to a list of embedded objects based on another value within the object. A sample document looks like:
{
"array" : [{
"val1" : "a"
}, {
"val1" : "b"
}]
}
What I am trying to achieve is
{
"array" : [{
"val1" : "a",
"isVal1A": true
}, {
"val1" : "b",
"isVal1A": false
}]
}
How do I go about doing this using an aggregate pipeline? Thanks!
Try
Live version
db.collection.aggregate({
$addFields: {
array: {
$map: {
input: "$array",
as: "a",
in: {
$cond: [
{
$eq: [
"$$a.val1",
"a"
]
},
{
"$mergeObjects": [
"$$a",
{
"Isval1A": true
}
]
},
{
"$mergeObjects": [
"$$a",
{
"Isval1A": false
}
]
}
]
}
}
}
}
})
You can try,
$map to iterate loop of array, check condition if val1 is 'a' then returnb true otherwise false, $mergeObjects to merge current object and status field
db.collection.aggregate([
{
$addFields: {
array: {
$map: {
input: "$array",
in: {
$mergeObjects: [
"$$this",
{
isVal1A: {
$cond: [{ $eq: ["$$this.val1", "a"] }, true, false]
}
}
]
}
}
}
}
}
])
Playground