How to remove deeply nested object (Node.js, mongoose) - javascript

I'm making a kanban task management app and I'm trying to remove a task with the _id: req.params.id which has the value of 62fa5ae05778ec97bc6ee23a. I tried the following:
const task = await Board.findOneAndUpdate(
{
"columns.tasks._id": req.params.id,
},
{ $pull: { "columns.$.tasks.$._id": req.params.id } },
{ new: true }
);
But I get the error Too many positional (i.e. '$') elements found in path'columns.$.tasks.$._id'
I searched for a while and came across arrayFilters from the docs but I'm struggling a lot to understand how to implement it for this particular need.
{
"_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": "62fa5ae05778ec97bc6ee23b"
},
{
"name": "do homework",
"completed": false,
"_id": "62fa5ae05778ec97bc6ee23c"
}
],
"_id": "62fa5ae05778ec97bc6ee23a"
}
]
},
{
"name": "doing",
"_id": "62fa5aa25778ec97bc6ee234",
"tasks": []
},
{
"name": "done",
"_id": "62fa5aa25778ec97bc6ee235",
"tasks": []
}
],
"__v": 0
}

You need to use $[] positional operator in order to pull from the nested array. Try running this query:
db.Board.updateOne({
"_id" : "62fa5aa25778ec97bc6ee231",
}, {
$pull: { 'columns.$[].tasks': { '_id': '62fa5ae05778ec97bc6ee23a' } }
});

Related

How to return single object (mongoose/mongoDB)

I have this data stored in database.
{
"_id": "62fa5aa25778ec97bc6ee231",
"user": "62f0eb5ebebd0f236abcaf9d",
"name": "Marketing Plan",
"columns": [
{
"name": "todo",
"_id": "62fa5aa25778ec97bc6ee233",
"tasks": [
{
"title": "Task Four testing 2",
"description": "This is task four",
"subtasks": [
{
"name": "wash dshes test",
"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
}
I want to be able to return a single object with the id equal to the req.params.id, in this case that would be 62ff74bfe80b11ade2d34455.
{
"title": "Task Four testing 2",
"description": "This is task four",
"subtasks": [
{
"name": "wash dshes test",
"completed": false,
"_id": "62ff74bfe80b11ade2d34456"
},
{
"name": "do homework",
"completed": false,
"_id": "62ff74bfe80b11ade2d34457"
}
],
"_id": "62ff74bfe80b11ade2d34455"
}
I researched stackoverflow and came across this potential solution: Mongoose retrieve one document from nested array which implemented the aggregate framework. But when I test this in postman, the request isn't made.
const getTask = asyncHandler(async (req, res) => {
const task = await Board.aggregate([
{
$match: {
"columns.tasks._id": req.params.id,
},
},
{
$project: {
columns: {
$first: {
$filter: {
input: "$columns.tasks",
cond: {
$eq: ["$$this._id", req.params.id],
},
},
},
},
},
},
{
$replaceRoot: {
newRoot: "$columns",
},
},
]);
});
Having an array inside an array complicates the query a bit, but here's one way to retrieve the data you want.
db.Board.aggregate([
{
$match: {
"columns.tasks._id": req.params.id
}
},
{"$unwind": "$columns"},
{
$match: {
"columns.tasks._id": req.params.id
}
},
{
"$project": {
"task": {
"$first": {
"$filter": {
"input": "$columns.tasks",
"cond": {"$eq": ["$$this._id", req.params.id]}
}
}
}
}
},
{"$replaceWith": "$task"}
])
Try it on mongoplayground.net. [The mongoplayground.net example uses "62ff74bfe80b11ade2d34455" rather than req.params.id.]

Mongodb: How to group the result of a $lookup and map them to a list objects that contain a matching key?

I have classroom document which contain 'modules', the whole document looks like this:
{
"_id": "628a7ea21e2a666d7872efbf",
"name": "Test Class",
"owner": "60763491b98b9e186ef33137",
"schoolId": "607dff27c712219af1e65d83",
"description": "This is a test class.",
"roster": [],
"modules": [
{
"name": "Test Module 1",
"id": "62a7082d0bf84c43fdfe95ff",
"isPublished": false
},
{
"name": "Test Module 2",
"id": "62a72d6378ce044dca32e1a2",
"isPublished": false
}
]
}
I also have assignment documents as such:
{
"classroomId": "628a7ea21e2a666d7872efbf",
"moduleId": "62a7082d0bf84c43fdfe95ff",
"name": "Assignment 1",
"description": "Test description",
"created": 1655120822055,
"reading": null,
"questions": [],
"isPublished": true,
"_id": "62a723b6683ffc4b11940c7b"
}
My question is how could do aggregation such that if I want to do a lookup on the assignments for the classroom, I am able to group the assignment documents by moduleId and then add them as a field to the modules array. The final document would like this:
{
"_id": "628a7ea21e2a666d7872efbf",
"name": "Test Class",
"owner": "60763491b98b9e186ef33137",
"schoolId": "607dff27c712219af1e65d83",
"description": "This is a test class.",
"roster": [],
"modules": [
{
"name": "Test Module 1",
"id": "62a7082d0bf84c43fdfe95ff",
"isPublished": false,
"assignments": [
{
"_id": "62a708ab0bf84c43fdfe9600",
"classroomId": "628a7ea21e2a666d7872efbf",
"moduleId": "62a7082d0bf84c43fdfe95ff",
"name": "Assignment 1",
"description": "Test description",
"created": 1655113899629,
"due": null,
"settings": null,
"reading": null,
"questions": [],
"isPublished": true
},
{
"_id": "62a723b6683ffc4b11940c7b",
"classroomId": "628a7ea21e2a666d7872efbf",
"moduleId": "62a7082d0bf84c43fdfe95ff",
"name": "Assignment 1",
"description": "Test description",
"created": 1655120822055,
"due": null,
"settings": null,
"reading": null,
"questions": [],
"isPublished": true
}
]
},
{
"name": "Test Module 2",
"id": "62a72d6378ce044dca32e1a2",
"isPublished": false,
"assignments": [
]
}
]
}
Right now I just have the base lookup and obviously this just gets me a separate assignments field, without groups.
lookup = [
{
$lookup: {
from: "assignments",
localField: "modules.id",
foreignField: "moduleId",
as: "assignments"
}
}
];
$lookup
$set - Set modules field.
2.1. $map - Iterate the modules array and returns a new array.
2.1.1. $mergeObject - Merge current iterate module document with the document with assignments array from the result 2.1.1.1.
2.1.1.1. $filter - Filter the assignments array by matching moduleId.
$unset - Remove assignments field.
db.classroom.aggregate([
{
$lookup: {
from: "assignments",
localField: "modules.id",
foreignField: "moduleId",
as: "assignments"
}
},
{
$set: {
modules: {
$map: {
input: "$modules",
as: "module",
in: {
$mergeObjects: [
"$$module",
{
"assignments": {
$filter: {
input: "$assignments",
as: "asgn",
cond: {
$eq: [
"$$module.id",
"$$asgn.moduleId"
]
}
}
}
}
]
}
}
}
}
},
{
$unset: "assignments"
}
])
Sample Mongo Playground

Mongodb $graphLookup aggregation inconsistent ouput order and sorting

I have this aggregation operation, and it's giving me the correct output, but with an inconsistent order. When I reload, the nested output array (posteriorThread) changes the order of the documents, and there seems to be no rhyme or reason!
I'm confused why the order keeps changing, and I would like to know why it's happening, but I figured I would just sort it, which I did, but I'm having trouble grouping it back together.
I'll show you both of my broken solutions below, but essentially I want output 1 but with the correct order. I'm using mongoose, but that shouldn't make a difference.
Thanks.
1: Inconsistent order solution
const posteriorThread = await Comment.aggregate([
{
$match: {
_id: post.threadDescendant,
},
},
{
$graphLookup: {
from: 'comments',
startWith:'$threadDescendant',
connectFromField: 'threadDescendant',
connectToField: '_id',
as: 'posteriorThread',
},
},
]);
OUTPUT: 1
posteriorThread [
{
"_id": "000",
"name": "foo bar",
"text": "testing one",
"threadDescendant": "123",
"posteriorThread": [
{
"_id": "234",
"name": "foo bar",
"text": "testing four",
"threadDescendant": "345"
},
{
"_id": "345",
"name": "foo bar",
"text": "testing three",
},
{
"_id": "123",
"name": "foo bar",
"text": "testing two",
"threadDescendant": "234"
},
]
}
]
2: Correct older but lose root document
const posteriorThread = await Comment.aggregate([
{
$match: {
_id: post.threadDescendant,
},
},
{
$graphLookup: {
from: 'comments',
startWith: '$threadDescendant',
connectFromField: 'threadDescendant',
connectToField: '_id',
as: 'posteriorThread',
},
},
{
$unwind: '$posteriorThread',
},
{
$sort: { 'posteriorThread.depth': 1 },
},
{
$group: { _id: '$_id', posteriorThread: { $push: '$posteriorThread' } },
},
]);
OUTPUT 2:
posteriorThread [
{
"_id": "000",
"posteriorThread": [
{
"_id": "123",
"name": "foo bar",
"text": "testing two",
"threadDescendant": "234"
},
{
"_id": "234",
"name": "foo bar",
"text": "testing four",
"threadDescendant": "345"
},
{
"_id": "345",
"name": "foo bar",
"text": "testing three",
},
]
}
]
The $graphLookup pipeline stage doesn't offer any built-in sorting capability, thus your second approach is correct. You just need to use $first in order to preserve root object's fields. You can use $replaceRoot and special $$ROOT variable to avoid specifying each field explicitly:
{
$group: {
_id: "$_id",
posteriorThread: { $push: "$posteriorThread" },
root: { $first: "$$ROOT" }
}
},
{
$project: {
"root.posteriorThread": 0
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
{ posteriorThread: "$posteriorThread" },
"$root"
]
}
}
}
Mongo Playground

MongoDB, Mongoose, How to remove duplicate items from the doc.find results after populating that data?

I have an array of items in my database, some of these items have an array called relatedItems, which are basically an array of IDs, and these IDs could match some of the items inside the same document. I can run populate to transform the array of IDs into an array of objects, where each object is the populated item with the matched ID. But I also want to remove those items from the main array of items. Here is the example of my data structure:
{
"data": {
"getItems": [
{
"title": "item 09",
"_id": "5a56215426004a22c17ba733",
"relatedItems": ["5a5621a526004a22c17ba773", "5a56214b26004a22c17ba72a", "5a56215326004a22c17ba732", "5a56215726004a22c17ba735"]
},
{
"title": "item 10",
"_id": "5a56215726004a22c17ba735",
"thumbnail": {
"Key": ".../5a56215726004a22c17ba735/thumbnail.jpg"
}
},
{
"title": "item 11",
"_id": "5a56215326004a22c17ba732",
"thumbnail": {
"Key": ".../5a56215326004a22c17ba732/thumbnail.jpg"
}
},
{
"_id": "5a56216b26004a22c17ba747",
"relatedItems": []
},
{
"_id": "5a56216c26004a22c17ba748",
"relatedItems": []
}
]
}
}
And I am running the following code to populate the data:
let items = await ItemModel.find({ category }).limit(limit).sort({ itemOrder: 1 })
.populate({path: 'relatedItems', select: '_id thumbnail'}).sort({ itemOrder: 1 });
But as you know, after populating, and I am going to end up having duplicate items (one copy in the related items array and one in the main array) like so:
{
"data": {
"getItems": [
{
"title": "item 09",
"_id": "5a56215426004a22c17ba733",
"relatedItems": [
"_id": "5a5621a526004a22c17ba773",
"thumbnail": {
"Key": ".../5a5621a526004a22c17ba773/thumbnail.jpg"
}
},
{
"_id": "5a56214b26004a22c17ba72a",
"thumbnail": {
"Key": ".../5a56214b26004a22c17ba72a/thumbnail.jpg"
}
},
{
"title": "item 11",
"_id": "5a56215326004a22c17ba732",
"thumbnail": {
"Key": ".../5a56215326004a22c17ba732/thumbnail.jpg"
}
},
{
"title": "item 10",
"_id": "5a56215726004a22c17ba735",
"thumbnail": {
"Key": ".../5a56215726004a22c17ba735/thumbnail.jpg"
}
}
]
},
{
"title": "item 10",
"_id": "5a56215726004a22c17ba735",
"thumbnail": {
"Key": ".../5a56215726004a22c17ba735/thumbnail.jpg"
}
},
{
"title": "item 11",
"_id": "5a56215326004a22c17ba732",
"thumbnail": {
"Key": ".../5a56215326004a22c17ba732/thumbnail.jpg"
}
},
{
"_id": "5a56216b26004a22c17ba747",
"relatedItems": []
},
{
"_id": "5a56216c26004a22c17ba748",
"relatedItems": []
}
]
}
}
I would like to remove the copy from the main array, and currently, I have accomplished it via the following code:
let items = await ItemModel.find({ category }).limit(limit).sort({ itemOrder: 1 })
.populate({path: 'relatedItems', select: '_id thumbnail'}).sort({ itemOrder: 1 });
Promise.all(items.map(async item => {
if (item.relatedItems && item.relatedItems.length > 0) {
Promise.all(item.relatedItems.map(async relatedItem => {
related.push(relatedItem._id);
}));
}
}));
items = items.filter(item => !JSON.stringify(related).includes(item._id));
And it works, but I am wondering if there is a faster/safer or more MongoDB native way to do this?
Edit
One important downside to using my filter method is that .limit(limit) does not work properly anymore since the array will be trimmed down if it finds duplicates, the workaround is using .slice(0, limit) on the final array after filtering, but doesn't sound like it's the best way. Also I kinda feel I am going to run into some issues if I decide to add pagination to it too.

Mongoose findOneAndUpdate inside nested Arrays

I need to update the type, key and values in the 'fields' array if the componentId and the wfInstanceId id matches.
This is the document that i need to do so.
{
"_id": {
"$oid": "586b6d756937c22f207dd5af"
},
"wfInstanceId": "0111",
"workflowId": "ash3",
"folderURL": "ash3",
"teamName": "teamName",
"dataStream": [
{
"componentInstanceId": "componentInstanceId1",
"componentId": "componentId1",
"_id": {
"$oid": "586b6d756937c22f207dd5b0"
},
"fields": [
{
"type": "String",
"value": "value1",
"key": "key",
"_id": {
"$oid": "586b6d756937c22f207dd5b1"
}
},
{
"type": "String",
"value": "value2",
"key": "key1",
"_id": {
"$oid": "586b6d756937c22f207dd5b1"
}
}
]
},
{
"componentInstanceId": "componentInstanceId2",
"componentId": "componentId22",
"_id": {
"$oid": "586b6d756937c22f207dd5b0"
},
"fields": [
{
"type": "String",
"value": "value1",
"key": "key",
"_id": {
"$oid": "586b6d756937c22f207dd5b1"
}
},
{
"type": "String",
"value": "value2",
"key": "key2",
"_id": {
"$oid": "586b6d756937c22f207dd5b1"
}
}
]
}
],
"id": "38f356f0-d196-11e6-b0b9-3956ed7f36f0",
"__v": 0
}
I tried this like this, Also i tried $set over $push which is also doesn't work.
Model.findOneAndUpdate( {'wfInstanceId': '0111', 'dataStream.componentId': 'componentId1'}, {$push : { 'dataStream.fields.$.key' : 'sdsdds' }}, { upsert: true }, function (err, data) {
if (err) {
reject(err);
console.log('error occured' + err);
}
console.info("succesfully saved");
resolve (data);
});
As they describe in this DOC it shouldbe working for update method that is also didn't work for me. Any help will be really appreciated to overcome this issue i'm facing.
As you are using $ operator it updates only the first matched array element in each document.
As of now it is not possible in mongoose to directly update all array elements.
You can do this in your case:
db.collection.find({'wfInstanceId': '0111', 'dataStream.componentId': 'componentId1'})
.forEach(function (doc) {
doc.datastream.forEach(function (datastream) {
if (dataStream.componentId === componentId1) {
dataStream.fields.forEach(function(fields){
// you can also write condition for matching condition in field
dataStream.fields.key="";
dataStream.fields.value="";
dataStream.fields.type="";
}
}
});
db.collection.save(doc);
});
It is normal javascript code. I think it's clearer for mongo newbies.

Categories

Resources