MongoDB update a document field in Array of Arrays - node.js

How to update ($set) a field "text" that matching "callback_data":"like" in document like this:
"data": {
"buttons": [
[{
"text": "Button",
"url": "https://example.org"
}],
[{
"text": "👍",
"callback_data": "like"
}, {
"text": "👎",
"callback_data": "dislike"
}]
]
}

Demo - https://mongoplayground.net/p/_g9YmDY5WMn
Use - update-documents-with-aggregation-pipeline
$map $mergeObjects $cond
db.collection.update({},
[
{
$set: {
"data.buttons": {
$map: {
input: "$data.buttons",
in: {
$map: {
input: "$$this", // array inside buttons
in: {
$cond: [
{ $eq: [ "$$this.callback_data", "like" ] }, // condition
{ $mergeObjects: [ "$$this", { text: "changed" } ] }, // true
"$$this" // false
]
}
}
}
}
}
}
}
],
{
multi: true
})

Related

Mongodb and nodejs find and filter nested objects

Firstly this is my Workspaces collection
[
{
"WorkspaceId": "er890we8rw98ro9we8rjower",
"WorkspaceTitle": "My First Workspace",
"WorkspaceOwner": "user1",
"Pages": [
{
"PageId": "a1sd32as7d841a23sd",
"PageMembers": [
{
"MemberId": "user1",
"MemberName": "John",
"MemberAvatar": "https://example.com/jKLa29Wqy",
"MemberAccess": "can edit"
},
{
"MemberId": "user2",
"MemberName": "Margot",
"MemberAvatar": "https://example.com/wKKLSAqy",
"MemberAccess": "can edit"
},
{
"MemberId": "user3",
"MemberName": "Silvia",
"MemberAvatar": "https://example.com/wKKLSAqy",
"MemberAccess": "can edit"
},
...
]
}
]
}
]
I wanr to print specific nested objects from document. I need to get a specific document inside my collection like this.
{
"WorkspaceId": "er890we8rw98ro9we8rjower",
"WorkspaceTitle": "My First Workspace",
"WorkspaceOwner": "user1",
"Pages": [
{
"PageId": "a1sd32as7d841a23sd",
"PageMembers": [
{
"MemberId": "user1",
"MemberName": "John",
"MemberAvatar": "https://example.com/jKLa29Wqy",
"MemberAccess": "can edit"
}
]
}
]
}
But when I run this script, my api prints all of collection. How can I achieve this?
Below is the query I am using:
db.findOne({ $or: [{ "WorkspaceOwner": member_id }, { "Pages.PageMembers.MemberId": member_id }] }, function (err, result) {
res.status(200).json(result);
});
You can achieve by using unwind and filter aggregation.
db.collection.aggregate([
{
"$unwind": "$Pages"
},
{
$addFields: {
"Pages.PageMembers": {
$filter: {
input: "$Pages.PageMembers",
as: "temp",
cond: {
$in: [
"$$temp.MemberId",
[
"user1"
]
]
}
}
}
}
}
])
Playground

multiple updates for an array with $push

consider the following document skeleton
{
_id: "615749dce3438547adfff9bc",
items: [
{
type: "shirt",
color: "red",
sizes: [
{
label: "medium",
stock: 10,
price: 20,
},
{
label: "large",
stock: 30,
price: 40,
}
]
},
{
type: "shirt",
color: "green",
sizes: [
{
label: "small",
stock: 5,
price: 3,
},
{
label: "medium",
stock: 5,
price: 3,
},
]
}
]
}
when a new item comes in, I want to insert a new document to items, unless an item exists with the same type and color as the new one, in this case I want only to merge sizes into that existing item's sizes.
sizes does not have to be unique.
I tried to use $push with upsert: true and arrayFilters but apparently $push ignores arrayFilters.
node with mongodb package.
Query1
filter to see if exists
if exists map to update, else add in the end
*2 array reads, but stil faster than query2
Test code here
db.collection.update({},
[
{
"$set": {
"newitem": {
"type": "shirt",
"color": "red",
"sizes": [
{
"label": "medium"
}
]
}
}
},
{
"$set": {
"found": {
"$ne": [
{
"$filter": {
"input": "$items",
"cond": {
"$and": [
{
"$eq": [
"$$this.type",
"$newitem.type"
]
},
{
"$eq": [
"$$this.color",
"$newitem.color"
]
}
]
}
}
},
[]
]
}
}
},
{
"$set": {
"items": {
"$cond": [
{
"$not": [
"$found"
]
},
{
"$concatArrays": [
"$items",
[
"$newitem"
]
]
},
{
"$map": {
"input": "$items",
"in": {
"$cond": [
{
"$and": [
{
"$eq": [
"$$this.type",
"$newitem.type"
]
},
{
"$eq": [
"$$this.color",
"$newitem.color"
]
}
]
},
{
"$mergeObjects": [
"$$this",
{
"sizes": {
"$concatArrays": [
"$$this.sizes",
"$newitem.sizes"
]
}
}
]
},
"$$this"
]
}
}
}
]
}
}
},
{
"$unset": [
"found",
"newitem"
]
}
])
Query2
(alternative solution)
reduce and do the update
if found keep the updated, else add in the end
*1 array read (but concat is slow, for big arrays, >500 members, if you have big arrays use query1)
*this is the normal way to do it, if we had a fast way to add in the end of the array, but we dont, so Query1 is faster
Test code here
db.collection.update({},
[
{
"$set": {
"newitem": {
"type": "shirt",
"color": "red",
"sizes": [
{
"label": "medium"
}
]
}
}
},
{
"$set": {
"items-found": {
"$reduce": {
"input": "$items",
"initialValue": {
"items": [],
"found": null
},
"in": {
"$cond": [
{
"$and": [
{
"$eq": [
"$$value.found",
null
]
},
{
"$eq": [
"$$this.type",
"$newitem.type"
]
},
{
"$eq": [
"$$this.color",
"$newitem.color"
]
}
]
},
{
"items": {
"$concatArrays": [
"$$value.items",
[
{
"$mergeObjects": [
"$$this",
{
"sizes": {
"$concatArrays": [
"$$this.sizes",
"$newitem.sizes"
]
}
}
]
}
]
]
},
"found": true
},
{
"items": {
"$concatArrays": [
"$$value.items",
[
"$$this"
]
]
},
"found": "$$value.found"
}
]
}
}
}
}
},
{
"$set": {
"items": {
"$cond": [
"$items-found.found",
"$items-found.items",
{
"$concatArrays": [
"$items-found.items",
[
"$newitem"
]
]
}
]
}
}
},
{
"$unset": [
"items-found",
"newitem"
]
}
])

How to update an array in mongodb by using another array as both identifier and value?

Imagine the is a document like this:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'name1',
},
{
_id: 'id2',
name: 'name2',
},
{
_id: 'id3',
name: 'name3'
}
]
}
I have an array like this:
const newData = [
{_id: 'id1', name: 'newName1'},
{_id: 'id2', 'name': 'newName2', family:'newFamily2'}
]
what I want is to update the array in the document using the corresponding _id and using it to add/update each element.
so my end result would be like:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'newName1',
},
{
_id: 'id2',
name: 'newName2',
family:'newFamily2'
},
{
_id: 'id3',
name: 'name3'
}
]
}
my guess was using The filtered positional operator but I am not sure if it's the correct way to go and how to do it.
thank you for your kind tips beforehand.
There is no straight way to add/update in array, you can use update with aggregation pipeline starting from MongoDB 4.2,
First of all, you need to convert _id from string to objectId type, if you are using mongoose npm you can use mongoose.Types.ObjectId method or if you are using mongodb npm you can use ObjectId method,
let newData = [
{ _id: 'id1', name: 'newName1' },
{ _id: 'id2', 'name': 'newName2', family:'newFamily2' }
];
let newIds = [];
newData = newData.map(n => {
n._id = ObjectId(n._id); // or mongoose.Types.ObjectId(n._id)
newIds.push(n._id); // for checking conditions
return n;
});
You can put query condition, and do below operations,
$map to iterate loop of users array, check condition if user._id is in input newIds then do update operation otherwise do insert operation
update operation:
$filter to iterate loop of input newData and filter already present object from input so we can update it
$arrayElemAt to get first object from above filtered array
$mergeObjects to merge current object with above input object
insert operation:
$filter to iterate loop of newData array and return not present object means new items in array of objects
$concatArrays to concat above new and updated result array
db.collection.updateOne(
{ _id: ObjectId("someIdblahbla") },
[{
$set: {
users: {
$concatArrays: [
{
$map: {
input: "$users",
as: "u",
in: {
$cond: [
{ $in: ["$$u._id", newIds] },
{
$mergeObjects: [
"$$u",
{
$arrayElemAt: [
{
$filter: {
input: newData,
cond: { $eq: ["$$this._id", "$$u._id"] }
}
},
0
]
}
]
},
"$$u"
]
}
}
},
{
$filter: {
input: newData,
cond: { $not: { $in: ["$$this._id", "$users._id"] } }
}
}
]
}
}
}]
)
Playground
Query1 (update(merge objects) existing members, doesn't add new members)
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"}] with you array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"users": {
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
}
],
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
}
}
}
])
Query2 (update(merge) if found, else push in the end)
Its like the above but finds the not-existing members,and push them in the end.Its a bit more slower and complicated
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"},{"_id": "id4","name": "newName4"}]
with your array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"yourarray": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
},
{
"_id": "id4",
"name": "newName4"
}
]
}
},
{
"$addFields": {
"new-ids": {
"$setDifference": [
{
"$map": {
"input": "$yourarray",
"as": "u",
"in": "$$u._id"
}
},
{
"$map": {
"input": "$users",
"as": "u",
"in": "$$u._id"
}
}
]
}
}
},
{
"$addFields": {
"users": {
"$concatArrays": [
{
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": "$yourarray",
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
},
{
"$filter": {
"input": "$yourarray",
"as": "u",
"cond": {
"$in": [
"$$u._id",
"$new-ids"
]
}
}
}
]
}
}
},
{
"$unset": [
"yourarray",
"new-ids"
]
}
])

Match two equal fields of arrays of same documents without $unwind

I have below workouts collection
[
{
"receiverWorkout": {
"exercises": [
{ "_id": "1", "reps": 0 },
{ "_id": "2", "reps": 4 }
]
},
"senderWorkout": {
"exercises": [
{ "_id": "2", "reps": 12 },
{ "_id": "1", "reps": 8 }
]
}
}
]
I need to match same _id from receiverWorkout and senderWorkout exercises and want to add a key won true or false whose reps are greater.
If you can see first element of receiverWorkout exercises({ "_id": "1", "reps": 0 }) is matching with the second element of the senderWorkout exercises({ "_id": "1", "reps": 8 }) because both have same _id
Expected output should be something like
[
{
"receiverWorkout": {
"exercises": [
{ "_id": "1", "reps": 0, "won": false },
{ "_id": "2", "reps": 4, "won": false }
]
},
"senderWorkout": {
"exercises": [
{ "_id": "2", "reps": 12, "won": true },
{ "_id": "1", "reps": 8, "won": true }
]
}
}
]
I want to avoid $unwind
Please help!!!
You can use below aggregation.
Single Pass
db.colname.aggregate([
{"$project":{
"exercises":{
"$reduce":{
"input":"$receiverWorkout.exercises",
"initialValue":{"receiverWorkout":[],"senderWorkout":[]},
"in":{"$let":{
"vars":{
"sen":{"$arrayElemAt":["$senderWorkout.exercises",{"$indexOfArray":["$senderWorkout.exercises._id","$$this._id"]}]}
},
"in":{
"receiverWorkout":{
"$concatArrays":[
"$$value.receiverWorkout",
[{"_id":"$$this._id","reps":"$$this.reps","won":{"$gt":["$$this.reps","$$sen.reps"]}}]
]
},
"senderWorkout":{
"$concatArrays":[
"$$value.senderWorkout",
[{"_id":"$$sen._id","reps":"$$sen.reps","won":{"$gt":["$$sen.reps","$$this.reps"]}}]
]
}
}
}}
}
}
}},
{"$project":{
"receiverWorkout.exercises":"$exercises.receiverWorkout",
"senderWorkout.exercises":"$exercises.senderWorkout"
}}
])
Without using $unwind
db.colname.aggregate(
{"$project":{
"receiverWorkout.exercises":{
"$map":{
"input":"$receiverWorkout.exercises",
"in":{
"$let":{
"vars":{"sen":{"$arrayElemAt":["$senderWorkout.exercises",{"$indexOfArray":["$senderWorkout.exercises._id","$$this._id"]}]}},
"in":{
"_id":"$$this._id",
"reps":"$$this.reps",
"won":{"$gt":["$$this.reps","$$sen.reps"]}
}
}
}
}
},
"senderWorkout.exercises":{
"$map":{
"input":"$senderWorkout.exercises",
"in":{
"$let":{
"vars":{"rec":{"$arrayElemAt":["$receiverWorkout.exercises",{"$indexOfArray":["$receiverWorkout.exercises._id","$$this._id"]}]}},
"in":{
"_id":"$$this._id",
"reps":"$$this.reps",
"won":{"$gt":["$$this.reps","$$rec.reps"]}
}
}
}
}
}
}})
With $unwind
db.colname.aggregate([
{"$unwind":"$senderWorkout.exercises"},
{"$unwind":"$receiverWorkout.exercises"},
{"$sort":{"receiverWorkout.exercises._id":1,"senderWorkout.exercises._id":1}},
{"$addFields":{
"receiverWorkout.exercises.won":{
"$cond":[
{"$gt":["$receiverWorkout.exercises.reps","$senderWorkout.exercises.reps"]},
true,
false
]
},
"senderWorkout.exercises.won":{
"$cond":[
{"$gt":["$receiverWorkout.exercises.reps","$senderWorkout.exercises.reps"]},
false,
true
]
}
}},
{"$group":{
"_id":"$_id",
"receiverWorkout":{"$addToSet":"$receiverWorkout.exercises"},
"senderWorkout":{"$addToSet":"$senderWorkout.exercises"}
}}
])
You don't need to use array index to achieve this. If I good understand, you need to know, for each entry in your senderWorkout.exercises array, if an element with the same _id is present in receiverWorkout.exercises array. Here's the way to do this :
db.collection.aggregate([
{
$addFields: {
"senderWorkout.exercises": {
$map: {
input: "$senderWorkout.exercises",
as: "ex",
in: {
$cond: {
if: {
$in: [
"$$ex._id",
"$receiverWorkout.exercises._id"
]
},
then: {
_id: "$$ex._id",
reps: "$$ex.reps",
result: true
},
else: {
_id: "$$ex._id",
reps: "$$ex.reps",
result: false
}
}
}
}
}
}
}
])
you can try it here

$eq not working with $unwind result mongoose

i've simple schema where Events.like store objectId of user. i'd like to match a particular user in array (Events.like) and add a flag isLiked according to match.
Events.aggregate([
{ $unwind: '$like'},
{
"$project":{
// "count" : {$size: "$like"},
"like":1,
"isLiked" :{ "$eq": [ "like", "593564b94642650d5b09f16b" ] },
// "isLiked" : { "$cond": [{ "$eq": [ "like", "593564b94642650d5b09f16b" ] }, true, false ] } }
}
}
], function(err, list) { }
this method always give me false. see results
"data": [
{
"_id": "593647ae9e10082982d3f7a2",
"like": "593563f66d2e9f0b84553fc3",
"isLiked": false
},
{
"_id": "593643a5a1e73a2841d3cddb",
"like": "593564b94642650d5b09f16b",
"isLiked": false
},
{
"_id": "593643a5a1e73a2841d3cddb",
"like": "593563f66d2e9f0b84553fc3",
"isLiked": false
}
]
can anyone tell me where i'm mistaking
########## update ##########
{ $unwind: '$like'},
{
"$project":{
"isLiked" :{ "$eq": [ "$like", mongoose.Types.ObjectId("593564b94642650d5b09f16b") ] },
}
},
{
$group: {
_id: '$_id',
'likes': { $sum: 1},
"like": { $push: '$like' },
"isLiked" : 1
}
}
then
MongoError: the group aggregate field 'isLiked' must be defined as an expression inside an object
i simple want below result
"data": [
{
"_id": "593647ae9e10082982d3f7a2",
"like": ["593563f66d2e9f0b84553fc3","593643a5a1e73a2841d3cddb" ],
"likes":2
"isLiked": true
},
{
"_id": "593643a5a1e73a2841d3cddb",
"like": "[593563f66d2e9f0b84553fc3"],
"likes":1,
"isLiked": false
}
]
Hi the user id you are passing is being passed as a string you will need to convert it to objectId. Try this
EDIT: fixed path of the field as pointed by the asker, #ShivShanker
Events.aggregate([
{ $unwind: '$like'},
{
"$project":{
// "count" : {$size: "$like"},
"like":1,
"isLiked" :{ "$eq": [ "$like", mongoose.Types.ObjectId("593564b94642650d5b09f16b") ] }
}
}
], function(err, list) { }

Resources