MongoDB query - unwind and match preserving null OR different value/ add a new field based on a condition - mongodb

If a have a following structure :
{
_id: 1,
name: 'a',
info: []
},
{
_id: 2,
name: 'b',
info: [
{
infoID: 100,
infoData: 'my info'
}
]
},
{
_id: 3,
name: 'c',
info: [
{
infoID: 200,
infoData: 'some info 200'
},
{
infoID: 300,
infoData: 'some info 300'
}
]
}
I need to query in such a way to obtain the documents where infoID is 100 showing the infoData, or nothing if info is empty, or contains subdocuments with infoID different from 100.
That is, I would want the following output:
{
_id: 1,
name: 'a',
infoData100: null
},
{
_id: 2,
name: 'b',
infoData100: 'my info'
},
{
_id: 3,
name: 'c',
infoData100: null
}
If I $unwind by info and $match by infoID: 100, I lose records 1 and 3.
Thanks for your responses.

Try below query :
Query :
db.collection.aggregate([
/** Adding a new field or you can use $project instead of addFields */
{
$addFields: {
infoData100: {
$cond: [
{
$in: [100, "$info.infoID"] // Check if any of objects 'info.infoID' has value 100
},
{
// If any of those has get that object & get infoData & assign it to 'infoData100' field
$let: {
vars: {
data: {
$arrayElemAt: [
{
$filter: {
input: "$info",
cond: { $eq: ["$$this.infoID", 100] }
}
},
0
]
}
},
in: "$$data.infoData"
}
},
null // If none has return NULL
]
}
}
}
]);
Test : MongoDB-Playground

Related

How to get data with a cleaner way using mongoose?

I'm filtering the data based on a Boolean savedBoolean , and if that Boolean is not being inputted I'm getting all the data, this code works for now. But how to do it in a cleaner way since I'm duplicating the code.
let filteredReviews : any | undefined;
if (savedBoolean === true || savedBoolean === false) {
filteredReviews = await Interviewee.aggregate([{
$project: {
_id: 0,
userId: 1,
'interviews.review': 1,
},
},
{
$unwind: '$interviews',
},
{
$match: {
userId: '4',
'interviews.review.saved': savedBoolean,
},
},
{
$group: {
_id: '$interviews.review._id',
review: {
$first: '$interviews.review',
},
},
},
]).skip((Number(page) - 1) * 3).limit(3);
}
if (savedBoolean === undefined) {
filteredReviews = await Interviewee.aggregate([{
$project: {
_id: 0,
userId: 1,
'interviews.review': 1,
},
},
{
$match: {
userId: '4',
},
},
{
$unwind: '$interviews',
},
]).skip((Number(page) - 1) * 3).limit(3);
}
In MongoDB, the db.collection.remove() method removes documents from a collection. You can remove all documents from a collection, remove all documents that match a condition, or limit the operation to remove just a single document.

How to make and requests in mongodb queries

I've worked on this for about an hour now and I can't figure anything out that works so sorry if this is obvious.
I want my query to only return results where every result matches, but right now it returns a result if at least one match is found.
My document looks like this...
{
country: 'Great Britain',
data: [
{
school: 'King Alberts',
staff: [
{
name: 'John',
age: 37,
position: 'Head Teacher'
},
{
name: 'Grace',
age: 63,
position: 'Retired'
},
{
name: 'Bob',
age: 25,
position: 'Receptionist'
}
]
},
{
school: 'St Johns',
staff: [
{
name: 'Alex',
age: 22,
position: 'Head of Drama'
},
{
name: 'Grace',
age: 51,
position: 'English Teacher'
},
{
name: 'Jack',
age: 33,
position: 'Receptionist'
}
]
},
// ... more schools
]
}
The query I'm currently making looks like...
{ 'data.staff.name': { $in: names } }
and the 'names' array that is being provided looks like ['Bob', 'Grace', 'John', 'Will', 'Tom']. Currently both schools are being returned when I make this query, I think it's because the 'names' array contains 'Grace' which is a name present at both schools and so the document it matching. Does anyone know if there's a query I could make so mongodb only returns the school object if every name in the 'names' array is a member of staff at the school?
You need to use the aggregation pipeline for this, after matching the document we'll just filter out the none matching arrays, like so:
db.collection.aggregate([
{
$match: {
"data.staff.name": {
$in: names
}
}
},
{
$addFields: {
data: {
$filter: {
input: "$data",
cond: {
$eq: [
{
$size: {
"$setIntersection": [
"$$this.staff.name",
names
]
}
},
{
$size: "$$this.staff"
}
]
}
}
}
}
}
])
Mongo Playground

mongo push to array in aggregation

I have the following code:
const newArray = [];
companies.items.forEach(async (item) => {
if (item.parentCompanyID) {
newArray.push({
updateOne: {
filter: { id: item?.parentCompanyID },
update: [
{
$push: {
branches: {
id: item?.id,
name: item.companyName,
parentId: item?.parentCompanyID,
type: item.companyType,
active: item.isActive,
number: item.companyNumber,
newlyAdded: { $eq: [{ $type: '$newlyAdded' }, 'missing'] },
},
},
},
],
upsert: true,
},
});
} else {
newArray.push({
updateOne: {
filter: { id: item?.id },
update: [
{
$set: {
id: item?.id,
name: item.companyName,
parentId: item?.parentCompanyID,
type: item.companyType,
active: item.isActive,
number: item.companyNumber,
newlyAdded: { $eq: [{ $type: '$newlyAdded' }, 'missing'] },
},
},
],
upsert: true,
},
});
}
});
await Company.bulkWrite(newArray);
This will go through company.items and for each will add updateOne into newArray which will the goes to bulkWrite.
My problem lies with $push as this needs to be in aggregation pipeline, and when i add the brackets around update it will break with MongooseError: Invalid update pipeline operator: "$push"
Iam sure the script could be simplified but iam still fairly new to mongoDB. What i need is this to insert to Company if the item hasnt got parentCompanyID, if it does have than push to branches array for the relevant Company with id of parentCompanyID.
Sample data from company.items array:
{
id: 5,
name: "Sports"
parentCompanyID: null
},
{
id: 51,
name: "Football"
parentCompanyID: 5
}
And MongoDB for COmpany should look like this:
{
id: 5,
name: "Sports",
parentCompanyID: null,
branches: [{
id: 51,
name: "Football",
parentCompanyID: 5
}]
}
Hope this makes sense. ANy help would be appreciated. I could not find any similar issue and only one i came accross was to use $concatArrays but this wouldnt work either.
Thank you
EDIT:
as per #Takis_ answer thsi now sort of works. Only problem is when $concatArrays does it jobs its not pushing into array as expected from $push. this is the result as of now, insted of branches being one array it has nested arrays. if there are more branches it follows same patter and it could end up with many nested arrays rather than 1 array of objects. any ideas?
{
"id": 29683585,
"name": "123",
"parentId": null,
"newlyAdded": true,
"branches": [
[
null,
{
"id": 29693873,
"name": "245",
"parentId": 29683585
}
],
{
"id": 29695646,
"name": "789",
"parentId": 29683585
}
]
}
This has now been sorted. Thanks to Takis and his pointing to $concatArrays i was able to make this works.
Working code is below:
newArray.push({
updateOne: {
filter: { id: item?.parentCompanyID },
update: [
{
$set: {
branches: {
$concatArrays: [
{ $ifNull: ['$branches', []] },
[
{
id: item?.id,
name: item.companyName,
parentId: item?.parentCompanyID,
type: item.companyType,
active: item.isActive,
number: item.companyNumber,
newlyAdded: { $ne: ['$newlyAdded', null] },
},
],
],
},
},
},
],
upsert: true,
},
});

How to pull array of documents from array of documents?

I have a document like this
{
users: [
{
name: 'John',
id: 1
},
{
name: 'Mark',
id: 2
},
{
name: 'Mike',
id: 3
},
{
name: 'Anna',
id: 4
}
]
}
and I want to remove users from the array with ids 2 and 4. To do that I execute the following code:
const documents = [
{
id: 2
},
{
id: 4
},
]
Model.updateOne({ document_id: 1 }, { $pull: { users: { $in: documents } } });
But it doesn't remove any user.
Could you say me what I'm doing wrong and how to achieve the needed result?
This works if you can redefine the structure of your documents array:
const documents = [2, 4]
Model.updateOne({ document_id: 1 }, { $pull: { users: { id: { $in: documents } } } })

Generate new field in $project from the value of another

I have a collection of users with a field named level with a numeric value of 0 to 3. I am trying to return a generated field with the textual representation of the user's level. So far I have this, but it replaces the level field.
{ $project: {
'_id': 1,
'name': 1,
'email': 1,
'username': 1,
'password': 1,
'registered': 1,
'level': {
$switch: {
branches: [
{ case: 0, then: 'Pending' },
{ case: 1, then: 'Regular' },
{ case: 2, then: 'User manager' },
{ case: 3, then: 'Administrator' }
],
default: 'Unknown'
}
},
}
I would like to have a field named levelName in my aggregation output. How do I do this? I tried this:
'levelName': {
$switch: {
branches: [
{ case: { level: 0 }, then: 'Pending' },
{ case: { level: 1 }, then: 'Regular' },
{ case: { level: 2 }, then: 'User manager' },
{ case: { level: 3 }, then: 'Administrator' }
],
default: 'Unknown'
}
},
but to no avail.
In projection, you can easily change field name. For example, in your data, if you want to change your name of level field to levelName, you can simply write new name in field and assign value of old fieldName using $fieldName.
{
$project: {
levelName: "$level" //new name is levelName, and is assigned value of original field name level.
}
}
Now, in your case, you can achieve this by above way, However, your $switch statement is invalid or you are using $switch in wrong way. You need to specify which field to compare in your case expression i.e. { case: {$eq:["$level", 0]}, then: 'Pending' } instead of { case: 0, then: 'Pending' }.
db.indexName.aggregate([
{
$project:
{
'_id': 1,
'name': 1,
'email': 1,
'username': 1,
'password': 1,
'registered': 1,
levelName: {
$switch: {
branches: [
{ case: {$eq:["$level", 0]}, then: 'Pending' },
{ case: {$eq:["$level", 1]}, then: 'Regular' },
{ case: {$eq:["$level", 2]}, then: 'User manager' },
{ case: {$eq:["$level", 3]}, then: 'Administrator' }
],
default: 'Unknown'
}
}
}
}
])