MongoDB query for nested elements on each level - mongodb

I have to find all documents in collection, also those nested, by one field that all of them have (common_field). In xpath it would be something like this: //*[#common_field='value']. How to do something like this in mongo?
{
"_id": ObjectId(
"5e3800700045c500cecffb00"
),
"common_field": "100281",
"other_field": "other",
"A": {
"common_field": "313000000",
"a_field": "a"
},
"B": {
"common_field": "213125",
"b_field": "bb",
"B": {
"common_field": "543534",
"b_field": "b"
},
"C": {
"common_field": "312312",
"c_field": "c"
}
}
}

I don't think you can achieve it natively with the Aggregation framework, you have to write a recursive function. This one should work:
var CheckField = function (doc, fieldName, value) {
var ret = false;
Object.keys(doc).forEach(function (key) {
if (key == fieldName && doc[key] == value) {
ret = true;
} else {
if (typeof doc[key] == "object")
ret = ret || CheckField(doc[key], fieldName, value);
}
});
return ret;
};
Then use the function like this:
var ObjIds = []
db.col.find({}).forEach(function (myDoc) {
if (CheckField(myDoc, "common_field", "313000000"))
ObjIds.push(myDoc._id);
})
Either print documents directly of use returned array as query filter:
db.col.find({ _id: { $in: ObjIds } })

Related

$in returning nothing when array is empty

This is my backend code for filters :
db.collection("users")
.find({
$or: [
{ job: { $in: myFilters.jobs } },
{ role: myFilters.role }
]
})
it works really well, when
myFilters.jobs = ["manager","user"]
The problem is when myFilters.jobs is an empty array [""], it should return all documents.
Currently, this is returning nothing, while I need it to return all docs :
myFilters.jobs = [""]
How could I do that, please ?
I've tried out this with no luck :
db.collection("users")
.find({
$or: [
$cond: {
if: { $ne: [myFilters.jobs, ""] },
then: { job: myFilters.jobs },
else: { job: { $in: myFilters.jobs } }
},
{ role: myFilters.role }
]
})
So, I'm trying to explain the solution I've found :
1st . In a node ws, I'm receiving filters, and cleaning them with this function :
This function removes emptys objects or arrays(It is when a user doesn't pick any filter inside of the app).
This is the JSON filters I'm receiving , please notice that, for example, role is empty, because the user has not chosen any role filter.
{"filters":{"role":"","jobs":["database"]}}
It goes through this function, so role gets deleted :
clean(myFilters);
function clean(obj) {
for (var propName in obj) {
if (
obj[propName] === null ||
obj[propName] === undefined ||
obj[propName] === "" ||
!obj[propName].length
) {
delete obj[propName];
}
}
}
2nd ,
Then, I'm pushing the filters dynamically like this :
var find = {};
find.$and = [];
if (myFilters.role) {
find.$and.push({ role: myFilters.role });
}
if (myFilters.jobs) {
find.$and.push({ job: { $in: myFilters.jobs } });
}
Finally , this is the db.collection call :
db.collection("users")
.find(find)
.toArray(function(err, docs) {
res.send(docs);
});
SO , when a user doesn't pick a filter, the clean function is removing emptys selections ... Maybe there are better solutions ?
You can adjust your condition according to the values you get like this:
Using $where:
db.collection("users")
.find({ $where: function() {
return myFilters.jobs.includes(this.job) || this.role === myFilters.role
}})
JS only:
const cond = {};
if(myFilters.jobs.length === 0) {
cond.role = myFilters.role;
} else {
cond.$or = [
{ job: { $in: myFilters.jobs } },
{ role: myFilters.role }
];
}
db.collection("users")
.find(cond)
Update according to the OP's answer:
db.collection("users")
.find({ $where: function() {
let filter = true;
if(myFilters.jobs && myFilters.jobs.length) filter = myFilters.jobs.includes(this.members);
if(myFilters.role) filter = filter && this.name === myFilters.role;
return filter;
}})

Building MongoDB query with conditions

I need to build a MongoDB query by pushing a new language if it does not exist in the array already. But if it exists I get an error this '$push' is empty. It is correct.
My question is how to build the query adding $push only when it is necessary?
let pushNewLanguage = {};
if (!profile.languages || (profile.languages && !profile.languages.find(l => l === languageId))) {
pushNewLanguage = { languages: languageId };
}
const profileUpdate = await
Profiles.rawCollection().update(
{ userId: this.userId },
{
$inc: { countPublishedPoems: 1 },
$push: pushNewLanguage
}
);
Remove the conditional logic and use $addtoSet instead of $push.
$addToSet will only add the item if it doesn’t exist already.
const profileUpdate = await
Profiles.rawCollection().update(
{ userId: this.userId },
{
$inc: { countPublishedPoems: 1 },
$addToSet: { languages: languageId }
}
);
Since you are writing Javascript, you can create a "base" update object, and then add the $push property if you need:
const update = {
$inc: { countPublishedPoems: 1 }
}
if (!profile.languages || (profile.languages && !profile.languages.find(l => l === languageId))) {
update["$push"] = { languages: languageId };
}
const profileUpdate = await
Profiles.rawCollection().update(
{ userId: this.userId },
update
);

Mongodb - remove null fields "recursively"?

This is a question based on MongoDb - remove all fields that are null. The referred post only gives solution that removes null fields at the top level. However, how can I remove the null fields that embedded?
Please note that I have no idea of the possible names of the null fields and their depth, so I think we have to iterate over each field of each document.
This is an example:
{
"id": 14770467,
"f1": "a",
"f2": null,
"f3": [
{
"id": 76946819,
"f4": null
}
]
}
I'm expecting something like this:
{
"id": 14770467,
"f1": "a",
"f3": [
{
"id": 76946819
}
]
}
Thanks.
try this
const remove = (data) => {
for (let key in data) {
const val = data[key];
if (val == null) {
delete data[key];
} else if (Array.isArray(val)) {
val.forEach((v) => {
remove(v);
});
}
}
return data;
}
db.getCollection('Collection').find({}).forEach((data) => {
data = remove(data);
db.getCollection('OtherCollection').insert(data);
//db.getCollection('Collection').save(data); // update same record
print(data);
})
Above was not working for me. But was inspiration to seek for more.
This worked (with MongoDB shell version v4.0.5):
const remove= (obj) => {
Object.keys(obj).forEach(key => {
if (obj[key] && typeof obj[key] === 'object') removeEmpty(obj[key]);
else if (obj[key] == null) delete obj[key];
});
};
db.getCollection('Collection').find({}).forEach((data) => {
remove(data);
db.getCollection('OtherCollection').insert(data);
})

Can I access the positional $ operator in projection of findOneAndUpdate

I have this query that works, but I want for the doc to only display network.stations.$ instead of the entire array. If I write fields: network.stations.$, I get an error. Is there a way for the doc only to return a single element from [stations]?
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"network.stations.$.free_bikes": req.body.free_bikes
}, {
new: true,
fields: "network.stations"
}, (err, doc) => console.log(doc))
// I want doc to somehow point only to a single station instead of
// several stations like it currently does.
The answer is "yes", but not in the way you are expecting. As you note in the question, putting network.stations.$ in the "fields" option to positionally return the "modified" document throws a specific error:
"cannot use a positional projection and return the new document"
This however should be the "hint", because you don't really "need" the "new document" when you know what the value was you are modifying. The simple case then is to not return the "new" document, but instead return it's "found state" which was "before the atomic modification" and simply make the same modification to the returned data as you asked to apply in the statement.
As a small contained demo:
const mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/test',
options = { useMongoClient: true };
const testSchema = new Schema({},{ strict: false });
const Test = mongoose.model('Test', testSchema, 'collection');
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
await Test.remove();
await Test.insertMany([{ a: [{ b: 1 }, { b: 2 }] }]);
for ( let i of [1,2] ) {
let result = await Test.findOneAndUpdate(
{ "a.b": { "$gte": 2 } },
{ "$inc": { "a.$.b": 1 } },
{ "fields": { "a.$": 1 } }
).lean();
console.log('returned');
log(result);
result.a[0].b = result.a[0].b + 1;
console.log('modified');
log(result);
}
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})();
Which produces:
Mongoose: collection.remove({}, {})
Mongoose: collection.insertMany([ { __v: 0, a: [ { b: 1 }, { b: 2 } ], _id: 59af214b6fb3533d274928c9 } ])
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 2
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 4
}
]
}
So I'm doing the modifications in a loop so you can see that the update is actually applied on the server as the next iteration increments the already incremented value.
Merely by omitting the "new" option, what you get is the document in the state which it was "matched" and it then is perfectly valid to return that document state before modification. The modification still happens.
All you need to do here is in turn make the same modification in code. Adding .lean() makes this simple, and again it's perfectly valid since you "know what you asked the server to do".
This is better than a separate query because "separately" the document can be modified by a different update in between your modification and the query to return just a projected matched field.
And it's better than returning "all" the elements and filtering later, because the potential could be a "very large array" when all you really want is the "matched element". Which of course this actually does.
Try changing fields to projection and then use the network.stations.$ like you tried before.
If your query is otherwise working then that might be enough. If it's still not working you can try changing the second argument to explicitly $set.
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"$set": {
"network.stations.$.free_bikes": req.body.free_bikes
}
}, {
new: true,
projection: "network.stations.$"
}, (err, doc) => console.log(doc))

sails: convert field in sql query to json

I have a query like this:
var sql = "select * from a left join b where a.id in (select field from c)";
Model.query(sql, function (err, result) {
return res.json({data:result});
});
which return result something like this:
{
"data": [
{
"field1": "[\"element 1\", \"element 1\"]",
...
}
]
}
you can see that "[\"element 1\", \"element 1\"]" is string and i don't like that. i want them to bo json. so the result should something like this:
{
"data": [
{
"field1": [\"element 1\", \"element 1\"],
...
}
]
}
how can i achieve that?
If your query represents something like a many-to-many relation, you could have a look at the .populate() function.
Otherwise you could do it like so:
var data = [{
"field1": "[\"element 1\", \"element 2\"]",
"field2": "[\"element 1\", \"element 2\"]",
}];
var parsedData = [];
data.forEach(function(item, index) {
var parsed = {};
for (var fieldName in item) {
try {
parsed[fieldName] = JSON.parse(item[fieldName]);
} catch (e) {
parsed[fieldName] = [];
}
}
parsedData.push(parsed);
});
console.log(parsedData);