This question already has an answer here:
How to update nested object in array (Mongoose/MongoDB)
(1 answer)
Closed 6 months ago.
I'm building a kanban task management app. So far I've been able to create a task and delete task but I can get my head around how to update a nested task object. I have searched the documentation there's no clear explanation.
I wanted to update the task with the id of 62ff74bfe80b11ade2d34455 - req.params.id with the req.body.
{
"_id": "62fa5aa25778ec97bc6ee231",
"user": "62f0eb5ebebd0f236abcaf9d",
"name": "Marketing Plan",
"columns": [
{
"name": "todo",
"_id": "62fa5aa25778ec97bc6ee233",
"tasks": [
{
"title": "Task Four",
"description": "This is task four",
"subtasks": [
{
"name": "wash dshes",
"completed": false,
"_id": "62ff74bfe80b11ade2d34456"
},
{
"name": "do homework",
"completed": false,
"_id": "62ff74bfe80b11ade2d34457"
}
],
"_id": "62ff74bfe80b11ade2d34455"
}
]
},
{
"name": "doing",
"_id": "62fa5aa25778ec97bc6ee234",
"tasks": []
},
{
"name": "done",
"_id": "62fa5aa25778ec97bc6ee235",
"tasks": []
}
],
"__v": 0
}
// #desc Edit task
// #route PUT /api/tasks/:id
// #access Private
const editTask = asyncHandler(async (req, res) => {
const { title, description, subtasks, status } = req.body;
const task = await Board.findOne({ "columns.tasks._id": req.params.id });
const taskStatus = await Board.findOne({
"columns.tasks._id": req.params.id,
"columns._id": status,
});
if (!task) {
res.status(400);
throw new Error("Task not found");
}
// Check for user
if (!req.user) {
res.status(401);
throw new Error("User not found");
}
// Make sure the logged in user matches the task user
if (task.user.toString() !== req.user.id) {
res.status(401);
throw new Error("User not authorized");
}
if (taskStatus) {
const updatedTask = await Board.findOneAndUpdate({
"columns.tasks._id": req.params.id,
});
}
task.columns.map(async (val) => {
// check if tasks id equals the status id in the request body
if (val._id.toString() === status) {
const updatedTask = await Board.findOneAndUpdate(
{
"columns.tasks._id": req.params.id,
},
{ $set: { "columns.$[].tasks": { _id: req.params.id } } },
{ new: true }
);
}
});
});
I had to do something similar recently and I couldn't find a way to update it using mongoose. So I have come up with a workaround.
After doing all your checks
// retrieve the column index you want to update (I assume you have only these 3 columns)
const columnIndex = _.findIndex(task.columns, { name: "todo" });
// retrieve the actual column object
const column = task.columns[columnIndex ];
// get the index of the specific task you want to update
const taskIndex = _.findIndex(column.tasks, { _id: req.params.id });
// manipulate whatever you want here on the specific task by its index
task.columns[columnIndex].tasks[taskIndex]._id = req.params.id
// then finally you need to update the whole columns array
await Board.updateOne({your query here}, {
$set: {
columns: task.columns
}
});
This is actually dirty, but your data is structured in a very difficult way.
I am using lodash in the example. you are free to use JavaScript's array methods
Related
I have a node.js server using express and mongoose and I also have a Json structure of :
[
{
"_id": "63dbaf5daabee478202ae59f",
"experimentId": "85a91abe-ef2f-416f-aa13-ec1bdf5d9766",
"experimentData": [
{
"scanId": 1652890241,
"scanData": [
{
"areaName": "A1",
"areaData": [],
"_id": "63dbaf94aabee478202ae5a5"
},
...
],
"_id": "63dbaf6caabee478202ae5a3"
},
...
],
"__v": 2
},
...
]
How can I create a query to return a single object from the scanData array like
{
"areaName": "A1",
"areaData": [],
"_id": "63dbb006e322869df811eea4"
}
The best I was able to do was:
// * Get all shapes in a well/area
// ! uses the experiment id, scan id and areaName
router.get("/:id/scan/:scanId/area/:areaName", async (req, res) => {
try {
const experiment = await Experiment.findOne({
experimentId: req.params.id,
experimentData: {
$elemMatch: {
scanId: req.params.scanId,
scanData: {
$elemMatch: {
areaName: req.params.areaName
}
}
}
}
}, {'experimentData.scanData.$': 1})
console.log(experiment)
if (!experiment || experiment.length === 0) res.status(404).json({})
else {
res.send(experiment.experimentData[0])
}
} catch (err) {
res.status(500).json({ message: err.message })
}
})
But that just returned the scanData array it would be great if I could go one level deeper and just get the object with the areaName.
I also tried some solutions with $aggregate but was not able to get any data displayed it kept returning an empty array
You can $match by your criteria layer-by-layer and $unwind to get the final scanData object in an aggregation pipeline. Use $replaceRoot to get only the scanData object.
db.collection.aggregate([
{
"$match": {
"experimentId": "85a91abe-ef2f-416f-aa13-ec1bdf5d9766"
}
},
{
"$unwind": "$experimentData"
},
{
"$match": {
"experimentData.scanId": 1652890241
}
},
{
"$unwind": "$experimentData.scanData"
},
{
"$match": {
"experimentData.scanData.areaName": "A1"
}
},
{
"$replaceRoot": {
"newRoot": "$experimentData.scanData"
}
}
])
Mongo Playground
I am using NodeJS to update a nested info in the database but I can't seem to figure this out.
Data in database
{
"id": 1,
"data": {
"__v": 0,
"_id": "5887e1d85c873e0011036889",
"text": "you have to change this text",
"type": "cat",
"used": true,
"user": "5a9ac18c7478810ea6c06381",
"source": "user",
"status": {
"feedback": "",
"verified": true,
"sentCount": 1
},
My code to update:
UpdateFacts: async function (req, res, next) {
const {text} = req.body
const {id} = req.params
if(!id){
return res.status(400).send({message:'please provide id'})
}
if(!Object.keys(req.body).length){
return res.status(400).send({message:'please provide text'})
}
const checkIfFactExist = await Facts.findOne({
where: {
id
}
})
if(!checkIfFactExist){
return res.status(404).send({message:'this id does not exist'})
}
try {
if(text){
checkIfFactExist.data.text = text
}
checkIfFactExist.save()
return res.status(200).send({message:'updated'})
} catch (error) {
return res.status(500).send(error.message)
}
Data is the column and text is the field am trying to change but it's not working.
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
}
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".
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;