MongoDB Remove or Limit fields conditional aggregation - mongodb

I am having some issue writing a find/aggregate mongo query where my requirement is to get all the documents but having condition like:
Suppose I have 2 documents:
{
_id: 5ccaa76939d95d395791efd2,
name: 'John Doe',
email: 'john.doe#foobar.com',
private: true
}
{
_id: 5ccaa76939d95d395791efd2,
name: 'Jane Doe',
email: 'jane.doe#foobar.com',
private: false
}
Now the query I am trying to get my head around is if the field private is true then when I query I must get all documents except email fields not included if private is true, like this:
{
_id: 5ccaa76939d95d395791efd2,
name: 'John Doe',
private: true
}
{
_id: 5ccaa76939d95d395791efd2,
name: 'Jane Doe',
email: 'jane.doe#foobar.com',
private: false
}
Tried $redact, $cond, $$PRUNE, $$DESCEND in aggregate() as well as came across $$REMOVE (looks like it is newest feature) but unable to get the required output. Please help me out with the Query

You can use $$REMOVE to remove a field from returned documents.
db.collection.aggregate([
{ "$addFields": {
"email": {
"$cond": [
{ "$eq": ["$private", true] },
"$$REMOVE",
"$email"
]
}
}}
])
MongoPlayground

Thank you Anthony Winzlet, his solution worked like a charm.
If anyone faces same problem and requires to include more than 1 fields, I am doing so by writing this method:
function condition(privateFieldLimitationArray, publicFieldLimitationArray) {
const condition = {};
privateFieldLimitationArray.map((d, i) => {
condition[d] = {
"$cond": [
{ "$eq": ["$private", true] },
"$$REMOVE",
publicFieldLimitationArray.includes(d) ? '$$REMOVE' : '$'+d
]
}
});
return condition;
}
Then, you can use the above function like:
const privateLimitationArray = ['updatedAt', 'createdAt', 'email', 'lname', 'friendslist', '__v'];
const publicLimitationArray = ['updatedAt', 'createdAt', '__v'];
YourModel.aggregate([
{
$match: {
// Your query to find by
}
}, {
"$addFields": condition(privateLimitationArray, publicLimitationArray)
}
])
.then(result => {
// handle result
})
.catch(error => {
// handle error
});

Related

Update or append to a subcollection in mongo

I have a collection containing a subcollection. In one request, I would like to update a record in the subcollection or append to it if a match doesn't exist. For a bonus point I would also like this update to be a merge rather than an overwrite.
A crude example:
// Schema
{
subColl: [
{
name: String,
value: Number,
other: Number,
},
];
}
// Existing record
{
_id : 123,
subColl: [
{name: 'John',
value: 10,
other: 20}
]
}
// example
const update = { _id: 123, name: 'John', other: 1000 };
const { _id, name, other } = update;
const doc = await Schema.findById(_id);
const idx = doc.subColl.findIndex(({ name: nameInDoc }) => nameInDoc === name);
if (idx >= 0) {
doc.subColl[idx] = { ...doc.subColl[idx], other };
} else {
doc.subColl.push({ name, other });
}
doc.save();
Currently I can achieve this result by pulling the record, and doing the update/append manually but I am assuming that achieving it with a pure mongo query would be much faster.
I have tried:
Schema.findOneAndUpdate(
{ _id: 123, 'subColl.name': 'John' },
{ $set: { 'subColl.$': [{ name: 'John', other: 1000 }] } }
)
but this won't handle the append behaviour and also doesn't merge the object with the existing record, rather it overwrites it completely.
I am not sure is there any straight way to do this in single query,
Update with aggregation pipeline starting from MongoDB v4.2,
$cond to check name is in subColl array,
true condition, need to merge with existing object, $map to iterate loop, check condition if matches condition then merge new data object with current object using $mergeObjects
false condition, need to concat arrays, current subColl array and new object using $concatArrays
const _id = 123;
const update = { name: 'John', other: 1000 };
Schema.findOneAndUpdate(
{ _id: _id },
[{
$set: {
subColl: {
$cond: [
{ $in: [update.name, "$subColl.name"] },
{
$map: {
input: "$subColl",
in: {
$cond: [
{ $eq: ["$$this.name", update.name] },
{ $mergeObjects: ["$$this", update] },
"$$this"
]
}
}
},
{ $concatArrays: ["$subColl", [update]] }
]
}
}
}]
)
Playground

Unable to add a object as a Sub field for another Existing JSON object

I am unable to add the following object:
[
{ 'option1':'opt1','option2':'opt2','option3':'opt3'},
]
as a sub filed to:
const question_list=await Questions.find({ $and: [{categoryid:categoryId},{ isDeleted: false }, { status: 0 }] }, { name: 1 });
question_list=[{"_id":"5eb167fb222a6e11fc6fe579","name":"q1"},{"_id":"5eb1680abb913f2810774c2a","name":"q2"},{"_id":"5eb16b5686068831f07c65c3","name":"q5"}]
I want the final Object to be as:
[{"_id":"5eb167fb222a6e11fc6fe579","name":"q1","options":[
{ 'option1':'opt1','option2':'opt2','option3':'opt3'},
]},{"_id":"5eb1680abb913f2810774c2a","name":"q2","options":[
{ 'option1':'opt1','option2':'opt2','option3':'opt3'},
]},{"_id":"5eb16b5686068831f07c65c3","name":"q5","options":[
{ 'option1':'opt1','option2':'opt2','option3':'opt3'},
]}]
what is the best possible solution?
You need to use aggregation-pipeline to do this, instead of .find(). As projection in .find() can only accept $elemMatch, $slice, and $ on existing fields : project-fields-from-query-results. So to add a new field with new data to documents, use $project in aggregation framework.
const question_list = await Questions.aggregate([
{
$match: {
$and: [{ categoryid: categoryId }, { isDeleted: false }, { status: 0 }]
}
},
{
$project: {
_id: 0,
name: 1,
options: [{ option1: "opt1", option2: "opt2", option3: "opt3" }]
}
}
]);
Test : mongoplayground

Return all documents only if all matches conditions

Looking for help with a query that returns either true or false (or an empty array when false or similar) I need to query a couple of documents by id in a collection and only return true if all documents match the query, if one or more documents don't match I need a false value returned.
If the documents looks like below with the checked value both true and false I would like a false/empty array value back from the query but if the checked are true in all I want a true or the whole array back.
If a regular find is more suitable i could use that.
I've tried with a regular $match but it only return the matched documents.
I do like this now but feels it could be done in a better way?
const coupons = await CouponModel.find({ id }, { checked: 1, _id: 0 });
const everyCouponIsChecked = coupons.every(data => data.checked === true);
Thanks.
Sample data:
[ { _id: 5e43e7831bc81503efa54c61,
id: 'foo',
checked: true,
},
{ _id: 5e43e7831bc81503efa54c61,
id: 'foo',
checked: true,
},{ _id: 5e43e7831bc81503efa54c61,
id: 'foo',
checked: false,
}]
const result = await MyModel.aggregate([
{
$match: {
id: 'foo',
checked: true,
},
},
]);
You can use a $group stage with null _id, then check if all elements checked field are true with $allElementsTrue operator.
Here's the query :
db.collection.aggregate([
{
$group: {
_id: null,
docs: {
$push: "$$ROOT"
}
}
},
{
$addFields: {
allTrue: {
$allElementsTrue: "$docs.checked"
}
}
},
{
$project: {
result: {
$cond: {
if: {
$eq: [
true,
"$allTrue"
]
},
then: "$docs",
else: "$allTrue"
}
}
}
}
])
If any checked field is false, result will equal to false, else result will be equal to the array of documents.
You can test it here
You could do with a query like this
const coupons = await CouponModel.find({ _id:id, checked:true });
const everyCouponIsChecked = coupons.length > 0;

Check if document exists in mongodb

This is how I check if a document exists:
var query = {};
if (req.body.id) {
query._id = {
$ne: new require('mongodb').ObjectID.createFromHexString(req.body.id)
};
}
Creditor.native(function(err, collection) {
collection.find({
$or: [{
name: req.body.name
}, {
sapId: req.body.sapId
},
query
]
}).limit(-1).toArray(function(err, creditors) {
if (creditors.length > 0) {
return res.send(JSON.stringify({
'message': 'creditor_exists'
}), 409);
} else {
return next();
}
})
});
To avoid that multiple documents exist with the same name or/and the same sapID I do this check on every creation/update of a document.
E.g. I want to update this document and give it a different name
{
name: 'Foo',
sapId: 123456,
id: '541ab60f07a955f447a315e4'
}
But when I log the creditors variable I get this:
[{
_id: 541a89a9bcf55f5a45c6b648,
name: 'Bar',
sapId: 3454345
}]
But the query should only match the same sapID/name. However there totally not the same. Is my query wrong?
You're currently finding docs where name matches OR sapId matches OR _id doesn't match. So that last clause is what's pulling in the doc you're seeing.
You probably mean to find docs where (name matches OR sapId matches) AND _id doesn't match.
collection.find({ $and: [
query,
{ $or: [{
name: req.body.name
}, {
sapId: req.body.sapId
}
] } ]
})
Or more simply:
collection.find({
_id: { $ne: require('mongodb').ObjectID.createFromHexString(req.body.id) },
$or: [{
name: req.body.name
}, {
sapId: req.body.sapId
}
]
})

mongodb return person if comments

I have a query to return posts and person content types.
var query = {
type: {
$in: ["post", "person"]
},
}
But I only want to return person if person has any comments, but posts always.
var query = {
type: {
$in: ["post", "person"]
},
// somehow make this only for person
// comments: {"$exists": true}
}
Is this possible or should I use map reduce?
Thanks.
var query = {
$or: [
{ type: "post" },
{ type: "person", comments: { $exists: true }},
]
}
This should work, I've just tested it:
db.things.find({$or: [ { type: "post" },
{ type: "person", comments: { $exists: true }} ]})
I've initially thought that it is not possible to use same field name in query twice, but it was just an issue with some drivers. here you can find discussion about this.
Also map/reduce usual not acceptable for real time queries, because it pretty slow.