Update subarray of objects in mongodb - javascript

I have this document in my database:
{
"_id": "ObjectId(...)",
"chapters": [
{
"_id": "ObjectId(...)",
"link": "128371.html",
"content": ""
}
]
}
The chapters array can have up to 3k items, and I have to populate each content attribute with some info. I want to be able to save the info I want inside the right object. Until now I was able to change the content attribute generally (in all items), but I am having trouble filtering it. This is what I managed to code using what I found in other questions:
let content = "Testing";
await models.ListNovel.updateOne(
{ link: novel_link },
{ $set: { "chapters.$[].content": content } }
);
I saw that { arrayFilters: [{ link: { $eq: chapter_link } }], multi: false } may work in some cases, but I don't use the link identifier in the update.
Thank you!
UPDATE
Similar to Suleyman's solution, I ended up with the following working code, I hope it may be useful for you.
await models.ListNovel.updateOne(
{ link: novel.link },
{ $set: { "chapters.$[elem].content": content } },
{
multi: true,
arrayFilters: [{ "elem.link": { $eq: chapter.link } }]
}
);

The condition in updateOne must match parent object, but you are using { link: novel_link } which belongs to the inner array object field, so it cannot find the document, and update doesn't happen.
To illustrate this, let's say your schema is like this:
const mongoose = require("mongoose");
const schema = new mongoose.Schema({
name: String,
chapters: [
new mongoose.Schema({
link: String,
content: String
})
]
});
module.exports = mongoose.model("ListNovel", schema);
Let's have this existing document in this collection:
{
"_id": "5e498a1fe21eea0e10690e39",
"name": "Novel1",
"chapters": [
{
"_id": "5e498a1fe21eea0e10690e3b",
"link": "128371.html",
"content": ""
},
{
"_id": "5e498a1fe21eea0e10690e3a",
"link": "222222.html",
"content": ""
}
],
"__v": 0
}
If we want to update this document's chapter with "link": "128371.html", first we need to find it with name or _id field, and update it using the filtered positional operator $.
router.put("/novels/:name", async (req, res) => {
const novel_link = "128371.html";
const content = "Testing";
const result = await ListNovel.findOneAndUpdate(
{ name: req.params.name },
{
$set: { "chapters.$[chapter].content": content }
},
{
arrayFilters: [{ "chapter.link": novel_link }],
new: true
}
);
res.send(result);
});
Here I used findOneAndUpdate to immediately retrieve the updated document, but you can also use the updateOne instead of findOneAndUpdate.
The result will be like this:
{
"_id": "5e498a1fe21eea0e10690e39",
"name": "Novel1",
"chapters": [
{
"_id": "5e498a1fe21eea0e10690e3b",
"link": "128371.html",
"content": "Testing" // => UPDATED
},
{
"_id": "5e498a1fe21eea0e10690e3a",
"link": "222222.html",
"content": ""
}
],
"__v": 0
}

Related

MongoDB conditional update array elements

I'm using Mongoose in a Node.js backend and I need to update a subset of elements of an array within a document based on a condition. I used to perform the operations using save(), like this:
const channel = await Channel.findById(id);
channel.messages.forEach((i) =>
i._id.toString() === messageId && i.views < channel.counter
? i.views++
: null
);
await channel.save();
I'd like to change this code by using findByIdAndUpdate since it is only an increment and for my use case, there isn't the need of retrieving the document. Any suggestion on how I can perform the operation?
Of course, channel.messages is the array under discussion. views and counter are both of type Number.
EDIT - Example document:
{
"_id": {
"$oid": "61546b9c86a9fc19ac643924"
},
"counter": 0,
"name": "#TEST",
"messages": [{
"views": 0,
"_id": {
"$oid": "61546bc386a9fc19ac64392e"
},
"body": "test",
"sentDate": {
"$date": "2021-09-29T13:36:03.092Z"
}
}, {
"views": 0,
"_id": {
"$oid": "61546dc086a9fc19ac643934"
},
"body": "test",
"sentDate": {
"$date": "2021-09-29T13:44:32.382Z"
}
}],
"date": {
"$date": "2021-09-29T13:35:33.011Z"
},
"__v": 2
}
You can try updateOne method if you don't want to retrieve document in result,
match both fields id and messageId conditions
check expression condition, $filter to iterate loop of messages array and check if messageId and views is less than counter then it will return result and $ne condition will check the result should not empty
$inc to increment the views by 1 if query matches using $ positional operator
messageId = mongoose.Types.ObjectId(messageId);
await Channel.updateOne(
{
_id: id,
"messages._id": messageId,
$expr: {
$ne: [
{
$filter: {
input: "$messages",
cond: {
$and: [
{ $eq: ["$$this._id", messageId] },
{ $lt: ["$$this.views", "$counter"] }
]
}
}
},
[]
]
}
},
{ $inc: { "messages.$.views": 1 } }
)
Playground

MongoDB - Update all entries in nested array only if they exist

I have a multilevel nested document (its dynamic and some levels can be missing but maximum 3 levels). I want to update all the children and subchildren routes if any. The scenario is same as in any Windows explorer, where all subfolders' route need to change when a parent folder route is changed. For eg. In the below example, If I am at route=="l1/l2a" and it's name needs to be edited to "l2c", then I will update it's route as route="l1/l2c and I will update all childrens' route to say "l1/l2c/l3a".
{
"name":"l1",
"route": "l1",
"children":
[
{
"name": "l2a",
"route": "l1/l2a",
"children":
[
{
"name": "l3a",
"route": "l1/l2a/l3a"
}]
},
{
"name": "l2b",
"route": "l1/l2b",
"children":
[
{
"name": "l3b",
"route": "l1/l2b/l3b"
}]
}
]
}
Currently I am able to go to a point and I am able to change its name and ONLY its route in the following manner:
router.put('/navlist',(req,res,next)=>{
newname=req.body.newName //suppose l2c
oldname=req.body.name //suppose l2a
route=req.body.route // existing route is l1/l2a
id=req.body._id
newroute=route.replace(oldname,newname); // l1/l2a has to be changed to l1/l2c
let segments = route.split('/');
let query = { route: segments[0]};
let update, options = {};
let updatePath = "";
options.arrayFilters = [];
for(let i = 0; i < segments.length -1; i++){
updatePath += `children.$[child${i}].`;
options.arrayFilters.push({ [`child${i}.route`]: segments.slice(0, i + 2).join('/') });
} //this is basically for the nested children
updateName=updatePath+'name'
updateRoute=updatePath+'route';
update = { $setOnInsert: { [updateName]:newDisplayName,[updateRoute]:newroute } };
NavItems.updateOne(query,update, options)
})
The problem is I am not able to edit the routes of it's children if any i.e it's subfolder route as l1/l2c/l3a. Although I tried using the $[] operator as follows.
updateChild = updatePath+'.children.$[].route'
updateChild2 = updatePath+'.children.$[].children.$[].route'
//update = { $set: { [updateChild]:'abc',[updateChild2]:'abc' } };
Its important that levels are customizable and thus I don't know whether there is "l3A" or not. Like there can be "l3A" but there may not be "l3B". But my code simply requires every correct path else it gives an error
code 500 MongoError: The path 'children.1.children' must exist in the document in order to apply array updates.
So the question is how can I apply changes using $set to a path that actually exists and how can I edit the existing route part. If the path exists, it's well and good and if the path does not exist, I am getting the ERROR.
Update
You could simplify updates when you use references.Updates/Inserts are straightforward as you can only the update target level or insert new level without worrying about updating all levels. Let the aggregation takes care of populating all levels and generating route field.
Working example - https://mongoplayground.net/p/TKMsvpkbBMn
Structure
[
{
"_id": 1,
"name": "l1",
"children": [
2,
3
]
},
{
"_id": 2,
"name": "l2a",
"children": [
4
]
},
{
"_id": 3,
"name": "l2b",
"children": [
5
]
},
{
"_id": 4,
"name": "l3a",
"children": []
},
{
"_id": 5,
"name": "l3b",
"children": []
}
]
Insert query
db.collection.insert({"_id": 4, "name": "l3a", "children": []}); // Inserting empty array simplifies aggregation query
Update query
db.collection.update({"_id": 4}, {"$set": "name": "l3c"});
Aggregation
db.collection.aggregate([
{"$match":{"_id":1}},
{"$lookup":{
"from":"collection",
"let":{"name":"$name","children":"$children"},
"pipeline":[
{"$match":{"$expr":{"$in":["$_id","$$children"]}}},
{"$addFields":{"route":{"$concat":["$$name","/","$name"]}}},
{"$lookup":{
"from":"collection",
"let":{"route":"$route","children":"$children"},
"pipeline":[
{"$match":{"$expr":{"$in":["$_id","$$children"]}}},
{"$addFields":{"route":{"$concat":["$$route","/","$name"]}}}
],
"as":"children"
}}
],
"as":"children"
}}
])
Original
You could make route as array type and format before presenting it to user. It will greatly simplify updates for you. You have to break queries into multiple updates when nested levels don’t exist ( ex level 2 update ). May be use transactions to perform multiple updates in atomic way.
Something like
[
{
"_id": 1,
"name": "l1",
"route": "l1",
"children": [
{
"name": "l2a",
"route": [
"l1",
"l2a"
],
"children": [
{
"name": "l3a",
"route": [
"l1",
"l2a",
"l3a"
]
}
]
}
]
}
]
level 1 update
db.collection.update({
"_id": 1
},
{
"$set": {
"name": "m1",
"route": "m1"
},
"$set": {
"children.$[].route.0": "m1",
"children.$[].children.$[].route.0": "m1"
}
})
level 2 update
db.collection.update({
"_id": 1
},
{
"$set": {
"children.$[child].route.1": "m2a",
"children.$[child].name": "m2a"
}
},
{
"arrayFilters":[{"child.name": "l2a" }]
})
db.collection.update({
"_id": 1
},
{
"$set": {
"children.$[child].children.$[].route.1": "m2a"
}
},
{
"arrayFilters":[{"child.name": "l2a"}]
})
level 3 update
db.collection.update({
"_id": 1
},
{
"$set": {
"children.$[].children.$[child].name": "m3a"
"children.$[].children.$[child].route.2": "m3a"
}
},
{
"arrayFilters":[{"child.name": "l3a"}]
})
I don't think its possible with arrayFilted for first level and second level update, but yes its possible only for third level update,
The possible way is you can use update with aggregation pipeline starting from MongoDB 4.2,
I am just suggesting a method, you can simplify more on this and reduce query as per your understanding!
Use $map to iterate the loop of children array and check condition using $cond, and merge objects using $mergeObjects,
let id = req.body._id;
let oldname = req.body.name;
let route = req.body.route;
let newname = req.body.newName;
let segments = route.split('/');
LEVEL 1 UPDATE: Playground
// LEVEL 1: Example Values in variables
// let oldname = "l1";
// let route = "l1";
// let newname = "l4";
if(segments.length === 1) {
let result = await NavItems.updateOne(
{ _id: id },
[{
$set: {
name: newname,
route: newname,
children: {
$map: {
input: "$children",
as: "a2",
in: {
$mergeObjects: [
"$$a2",
{
route: { $concat: [newname, "/", "$$a2.name"] },
children: {
$map: {
input: "$$a2.children",
as: "a3",
in: {
$mergeObjects: [
"$$a3",
{ route: { $concat: [newname, "/", "$$a2.name", "/", "$$a3.name"] } }
]
}
}
}
}
]
}
}
}
}
}]
);
}
LEVEL 2 UPDATE: Playground
// LEVEL 2: Example Values in variables
// let oldname = "l2a";
// let route = "l1/l2a";
// let newname = "l2g";
else if (segments.length === 2) {
let result = await NavItems.updateOne(
{ _id: id },
[{
$set: {
children: {
$map: {
input: "$children",
as: "a2",
in: {
$mergeObjects: [
"$$a2",
{
$cond: [
{ $eq: ["$$a2.name", oldname] },
{
name: newname,
route: { $concat: ["$name", "/", newname] },
children: {
$map: {
input: "$$a2.children",
as: "a3",
in: {
$mergeObjects: [
"$$a3",
{ route: { $concat: ["$name", "/", newname, "/", "$$a3.name"] } }
]
}
}
}
},
{}
]
}
]
}
}
}
}
}]
);
}
LEVEL 3 UPDATE: Playground
// LEVEL 3 Example Values in variables
// let oldname = "l3a";
// let route = "l1/l2a/l3a";
// let newname = "l3g";
else if (segments.length === 3) {
let result = await NavItems.updateOne(
{ _id: id },
[{
$set: {
children: {
$map: {
input: "$children",
as: "a2",
in: {
$mergeObjects: [
"$$a2",
{
$cond: [
{ $eq: ["$$a2.name", segments[1]] },
{
children: {
$map: {
input: "$$a2.children",
as: "a3",
in: {
$mergeObjects: [
"$$a3",
{
$cond: [
{ $eq: ["$$a3.name", oldname] },
{
name: newname,
route: { $concat: ["$name", "/", "$$a2.name", "/", newname] }
},
{}
]
}
]
}
}
}
},
{}
]
}
]
}
}
}
}
}]
);
}
Why separate query for each level?
You could do single query but it will update all level's data whenever you just need to update single level data or particular level's data, I know this is lengthy code and queries but i can say this is optimized version for query operation.
you can't do as you want. Because mongo does not support it. I can offer you to fetch needed item from mongo. Update him with your custom recursive function help. And do db.collection.updateOne(_id, { $set: data })
function updateRouteRecursive(item) {
// case when need to stop our recursive function
if (!item.children) {
// do update item route and return modified item
return item;
}
// case what happen when we have children on each children array
}

findOne mongoose query is not working properly

I have used express to create this web-app.
I also have mongoose model:
{
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
notes: [{
name: { type: String, required: true }
}]
}
When I try to find object inside of array(Notes) ->
modelName.findOne({ "notes:" {$elemMatch: {"_id": req.body.id } }})
.exec()
.then(result => {
console.log(result);
})
.catch(err => {
console.log(err);
});
I get whole model object of one user instead of one note.
Here is the result which I get:
{
"_id": "5c51dd8be279e9016716e5b9",
"username": "user1",
"password": "",
"notes": [
{
"_id": "5c51dd8be279e901671gagag",
"name": "name of note1"
},
{
"_id": "5c51ff8be279e901671gagag",
"name": "name of note"
},
{
"_id": "5c51dd8be2131501671gagag",
"name": "name of note"
}
]
}
My expectation, however, is to receive something like this:
{
"_id": "5c51dd8be279e901671gagag",
"name": "name of note1"
}
P.S: It is not duplicate of this answer Mongoose Mongodb querying an array of objects. I have already tried to use code from that question, but it doesn't solve my problem
findOne() is working just fine. findOne() returns any document that matches the specified query, not part of a document. If you want just part of that document, you will have to get it in two parts...
modelName.findOne({ "notes": {$elemMatch: {"_id": req.body.id } }})
.exec()
.then(result => {
// Will get an array of notes whose _id === req.body.id
const resNote = result.notes.filter(n => n._id === req.body.id);
console.log(resNote);
})
.catch(err => {
console.log(err);
});
See the documentation here. If you note, it mentions that the function " finds one document".

how to populate nested array ref? mongoose

I found this post which is quite close to my need but somehow I still can't get it to work though
Populate nested array in mongoose
It's a bit hard to explain what kind of nested ref I am talking about. I just start with the code
I have a Products Schema
const ProductSchema = new Schema(Object.assign({
name: {type: String};
});
an order schema
const OrderSchema = new Schema(Object.assign({
products: [ {
product: { type: Schema.Types.ObjectId, ref: 'Products' },
amount: { type: Number },
total: { type: Number },
} ],
});
I tried doing
const order = await Orders.findOne({
_id: 'orderId39843984203'
}).populate({
path: 'products',
populate: {
path: 'product'
}
});
I tried something like that, and few other ways such as path: products.product or path: products.product._id and something simliar
but all I can get is the _id, it doesn't populate the whole thing.
Can someone please give me a hand or advice how this would work?
Thanks in advance
EDIT: this is how the document looks like in db for orderSchema
{
"_id": {
"$oid": "5ba2e2af52f2ff3f4226015c"
},
"products": [
{
"_id": {
"$oid": "5ba2e2ac52f22f3f4226015e"
},
"amount": 4,
"total": 2940
},
{
"_id": {
"$oid": "5ba2e2ac52f2ff3f5226015d"
},
"amount": 1,
"total": 840
}
],
"createdAt": {
"$date": "2018-09-19T23:58:36.339Z"
},
"updatedAt": {
"$date": "2018-09-19T23:58:36.505Z"
},
"__v": 0
}
.populate({ path: 'nested', populate: { path: 'deepNested' }});
where nested is first level ref and deepnested is ref of first level of ref.
You should be able to do it with this:
const order = await Orders.findOne({
_id: 'orderId39843984203'
}).populate('products.product')
.exec((error, doc) => doc);
As per the docs for populate

Aggregate match pipeline not equal to in MongoDB

I am working on an aggregate pipeline for MongoDB, and I am trying to retrieve items where the user is not equal to a variable.
For some reason, I couldn't make it work. I tried to use $not, $ne and $nin in different possible way but can't make it to work.
This is how it looks like:
Data sample:
[{
"_id": { "$oid": "565674e2e4b030fba33d8fdc" },
"user": { "$oid": "565674832b85ce78732b7529" }
}, {
"_id": { "$oid": "565674e2e4b030fba33d8fdc" },
"user": { "$oid": "565674832b85ce78732b7529" }
}, {
"_id": { "$oid": "565674e2e4b030fba33d8fdc" },
"user": { "$oid": "56f9dfc5cc03ec883f7675d0" }
}]
Pipeline sample (simplified for this question):
Where req.query.user.id = "565674832b85ce78732b7529"
collection.aggregate([
{
$match: {
user: {
$nin: [ req.query.user.id ],
}
}
}
]
This should return only the last item.
Do you have any idea how to retrieve the data that doesn't match the user?
Thanks
Edit:
The following doesn't work either:
collection.aggregate([
{
$match: {
'user.$oid': {
$nin: [ req.query.user.id ],
}
}
}
]);
I also tried with ObjectID() and mongodb complains: [MongoError: Argument must be a string]
var ObjectID = require('mongodb').ObjectID;
// Waterline syntax here
MyCollection.native(function (err, collection) {
collection.aggregate([
{
$match: {
'user': {
$nin: [ ObjectID(req.query.user.id) ],
}
}
}
], function (err, result) {
console.log(err, result);
});
});
But this line works in the shell:
db.collection.aggregate([{$match:{"user":{"$nin":[ObjectId("565674832b85ce78732b7529")]}}}])
Based on the answer here, you can change
var ObjectId = require('mongodb'). ObjectID;
to
var ObjectId = require('sails-mongo/node_modules/mongodb').ObjectID;

Categories

Resources