Mongodb - convert string toDate inside an array using updateMany - mongodb

My collection document looks like this. I would like to convert "due_date" from string to date format using "$toDate".
{
"item": 1,
"checklist": [{
"due_date": null,
"is_completed": false,
"is_deleted": false
},
{
"due_date": "2021-11-16T00:45:54.685Z",
"is_completed": false,
"is_deleted": false
},
]
},
{
"item": 2,
"checklist": [{
"due_date": "",
"is_completed": false,
"is_deleted": false
},
{
"due_date": "2022-1-16T00:45:54.685Z",
"is_completed": false,
"is_deleted": false
},
]
}
I was able to convert empty string to null using this query.
db.collection.updateMany({
"checklist.due_date": ""
},
{
"$set": {
"checklist.$[check].due_date": null
}
},
{
arrayFilters: [
{
"check.due_date": ""
}
]
})
When I try to update the date using similar method, it saves "$$checklist.due_date" in string format instead of actual date.
db.collection.updateMany({
"checklist.due_date": {
"$type": "string",
"$ne": ""
}
},
{
"$set": {
"checklist.$[check].due_date": {
$toDate: "$$checklist.due_date"
}
}
},
{
arrayFilters: [
{
"check.due_date": {
"$type": "string",
"$ne": ""
}
}
]
})
I have tried "$map" to update "due_date" but don't know how to filter out null values inside the object. It is giving me error while converting null values.
How to update date string in array to date format in mongoDB?

You can perform the conversion with $convert and set onError and onNull clauses to cater the null and empty string values. Use $map to perform the operation on all array elements.
db.collection.update({},
[
{
"$addFields": {
"checklist": {
"$map": {
"input": "$checklist",
"as": "cl",
"in": {
"$mergeObjects": [
"$$cl",
{
due_date: {
"$convert": {
"input": "$$cl.due_date",
"to": "date",
"onError": null,
"onNull": null
}
}
}
]
}
}
}
}
}
],
{
multi: true
})
Here is the Mongo Playground for your reference.

Related

Change value of a field in embed document with condition in mongo DB

I try to get a data from the database. But I've had a problem in a section where I can't edit the value of the embedded fields. I want to put the boolean value instead of the mobile number. if it has a value, equal to the true and if it does not have a value, it will be false.
I have document like this in my collection:
{
"_id": ObjectId("606d6ea237c2544324925c61"),
"title": "newwww",
"message": [{
"_id": ObjectId("606d6e1037c2544324925c5f"),
"text": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"user_id": null,
"user_full_name": null,
"user_mobile_number": null,
"submit_date": {
"$date": "2021-04-07T08:32:16.093Z"
}
}, {
"_id": ObjectId("606d6edc546feebf508d75f9"),
"text": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"user_id": null,
"user_full_name": null,
"user_mobile_number": "9653256482",
"submit_date": {
"$date": "2021-04-07T08:35:40.881Z"
}
}],
"user_mobile_number": "9652351489",
}
Do query:
db.ticket.aggregate([{"$match": {"_id": ObjectId("606d6ea237c2544324925c61")}}, {
"$project": {"message.is_admin":{
"$let": {
vars: {
mobile_number: "$message.user_mobile_numebr"
},
in: {
"$cond": [{$eq: ['$$mobile_number', null]},false,true ]
}
}
}
}
}])
and result is:
[
{
"_id": ObjectId("606d6ea237c2544324925c61"),
"message": [
{
"is_admin": true
},
{
"is_admin": true
}
]
}
]
but i want result like this:
[
{
"_id": ObjectId("606d6ea237c2544324925c61"),
"message": [
{
"is_admin": false
},
{
"is_admin": true
}
]
}
]
means I want when message.user_mobile_number has value, get true and when value is null get false.
How can I do this?
Demo - https://mongoplayground.net/p/h_zkRfDbZrS
Use $map
db.collection.aggregate([
{
"$project": {
"message": {
"$map": {
input: "$message",
"as": "message",
"in": {
"$cond": [{ "$eq": [ "$$message.user_mobile_number", null ] }, { is_admin: false }, { is_admin: true } ]
}
}
}
}
}
])

Mongodb aggregation with $addFileds and condition

Given that:
db :
{
id:"112",
val1: {val:""},
val2: {val:"123"},
}
I would like to run a script that updates a new field according to the aggregation result. The result is true if one of the values (val1, val2) is empty
The below is what I did with aggregation and then I would go over with for and update all rows:
db.valTest.aggregate(
[{
"$addFields": {
"val.selected": {
'$or': [{
'val1.val': ''
}, {
'val2.val': ''
}]
}
}
},
{
"$group": {
"_id": "$_id",
"id": {
"$first": "$id"
},
"value": {
"$first": "val1.val"
},
"result": {
"$push": {
"val": "val1.val",
"selected": "val.selected"
}
}
}
}
]
)
But, I do not get the correct result. I would like to get result like:
{
id:"112",
val1: {val:""},
val2: {val:"123"},
result: true
},
{
id:"114",
val1: {val:"4545"},
val2: {val:"123"},
result: false
}
Presently, I am getting the following error:
"message" : "FieldPath field names may not contain '.'.",
You need to use $eq aggregation operator for the matching criteria
db.collection.aggregate([
{ "$addFields": {
"result": {
"$cond": [
{ "$or": [{ "$eq": ["$val1.val", ""] }, { "$eq": ["$val2.val", ""] }] },
true,
false
]
}
}}
])

findOneAndUpdate property in array of objects

I'm trying to update a property in an array of objects - the find part already works. signedUp should be set to true.
Here's my method:
function lookupReferral(email) {
return getConnection().then(db => db.collection('Referrals').findOneAndUpdate(
{
emails: {$elemMatch: {name: email}}
},
{
$set: {
emails: {$elemMatch: {signedUp: true}},
updated: new Date()
}
}));
}
My idea in the $set clause is that I specify the particular object once again, and then the property that I'm setting, but it doesn't work. For further context, here's the record:
{
"_id": {
"$oid": "5b60504420f8e626148494e4"
},
"accountCode": "auth0|5b4de18d8bed60110409ded5",
"accountEmail": "example#gmail.com",
"emails": [
{
"name": "azzz#zzz.dk",
"signedUp": false
},
{
"name": "ds#d.dk",
"signedUp": false
},
{
"name": "ds#d.dk",
"signedUp": false
},
{
"name": "ds#d.dk",
"signedUp": false
}
],
"created": {
"$date": "2018-07-31T12:04:20.625Z"
},
"updated": {
"$date": "2018-07-31T12:04:20.625Z"
}
}
You need to use $ positional operator to update an array element
db.collection('Referrals').findOneAndUpdate(
{ "emails": { "$elemMatch": { "name": email }}},
{ "$set": { "emails.$.signedUp": true, "emails.$.updated": new Date() }}
)

Filter subdocument by datetime

I've the following model
var messageSchema = new Schema({
creationDate: { type: Date, default: Date.now },
comment: { type: String },
author: { type: Schema.Types.ObjectId }
});
var conversationSchema = new Schema({
title: { type: String },
author: { type : Schema.Types.ObjectId },
members: [ { type: Schema.Types.ObjectId } ],
creationDate: { type: Date, default: Date.now },
lastUpdate: { type: Date, default: Date.now },
comments: [ messageSchema ]
});
I want to create two methods to get the comments generated after a date by user or by conversationId.
By User
I tried with the following method
var query = {
members : { $all : [ userId, otherUserId ], "$size" : 2 }
, comments : { $elemMatch : { creationDate : { $gte: from } } }
};
When there are no comments after the specified date (at from) the method returns [] or null
By conversationId
The same happen when I try to get by user id
var query = { _id : conversationId
, comments : { $elemMatch : { creationDate : { $gte: from } } }
};
Is there any way to make the method returns the conversation information with an empty comments?
Thank you!
Sounds like a couple of problems here, but stepping through them all
In order to get more than a single match "or" none from an array to need the aggregation framework of mapReduce to do this. You could try "projecting" with $elemMatch but this can only return the "first" match. i.e:
{ "a": [1,2,3] }
db.collection.find({ },{ "$elemMatch": { "$gte": 2 } })
{ "a": [2] }
So standard projection does not work for this. It can return an "empty" array but it an also only return the "first" that is matched.
Moving along, you also have this in your code:
{ $all : [ userId, otherUserId ], "$site" : 2 }
Where $site is not a valid operator. I think you mean $size but there are actuall "two" operators with that name and your intent may not be clear here.
If you mean that the array you are testing must have "only two" elements, then this is the operator for you. If you meant that the matched conversation between the two people had to be equal to both in the match, then $all does this anyway so the $size becomes redundant in either case unless you don't want anyone else in the conversation.
On to the aggregation problem. You need to "filter" the content of the array in a "non-destructive way" in order to get more than one match or an empty array.
The best approach for this is with modern MongoDB features available from 2.6, which allows the array content to be filtered without processing $unwind:
Model.aggregate(
[
{ "$match": {
"members": { "$all": [userId,otherUserId] }
}},
{ "$project": {
"title": 1,
"author": 1,
"members": 1,
"creationDate": 1,
"lastUpdate": 1,
"comments": {
"$setDifference": [
{ "$map": {
"input": "$comments",
"as": "c",
"in": { "$cond": [
{ "$gte": [ "$$c.creationDate", from ] },
"$$c",
false
]}
}},
[false]
]
}
}}
],
function(err,result) {
}
);
That uses $map which can process an expression against each array element. In this case the vallues are tested under the $cond ternary to either return the array element where the condition is true or otherwise return false as the element.
These are then "filtered" by the $setDifference operator which essentially compares the resulting array of $map to the other array [false]. This removes any false values from the result array and only leaves matched elements or no elements at all.
An alternate may have been $redact but since your document contains "creationDate" at multiple levels, then this messes with the logic used with it's $$DESCEND operator. This rules that action out.
In earlier versions "not destroying" the array needs to be treated with care. So you need to do much the same "filter" of results in order to get the "empty" array you want:
Model.aggregate(
[
{ "$match": {
"$and": [
{ "members": userId },
{ "members": otherUserId }
}},
{ "$unwind": "$comments" },
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"author": { "$first": "$author" },
"members": { "$first": "$members" },
"creationDate": { "$first": "$creationDate" },
"lastUpdate": { "$first": "$lastUpdate" },
"comments": {
"$addToSet": {
"$cond": [
{ "$gte": [ "$comments.creationDate", from ] },
"$comments",
false
]
}
},
"matchedSize": {
"$sum": {
"$cond": [
{ "$gte": [ "$comments.creationDate", from ] },
1,
0
]
}
}
}},
{ "$unwind": "$comments" },
{ "$match": {
"$or": [
{ "comments": { "$ne": false } },
{ "matchedSize": 0 }
]
}},
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"author": { "$first": "$author" },
"members": { "$first": "$members" },
"creationDate": { "$first": "$creationDate" },
"lastUpdate": { "$first": "$lastUpdate" },
"comments": { "$push": "$comments" }
}},
{ "$project": {
"title": 1,
"author": 1,
"members": 1,
"creationDate": 1,
"lastUpdate": 1,
"comments": {
"$cond": [
{ "$eq": [ "$comments", [false] ] },
{ "$const": [] },
"$comments"
]
}
}}
],
function(err,result) {
}
)
This does much of the same things, but longer. In order to look at the array content you need to $unwind the content. When you $group back, you look at each element to see if it matches the condition to decide what to return, also keeping a count of the matches.
This is going to put some ( one with $addToSet ) false results in the array or only an array with the entry false where there are no matches. So yo filter these out with $match but also testing on the matched "count" to see if no matches were found. If no match was found then you don't throw away that item.
Instead you replace the [false] arrays with empty arrays in a final $project.
So depending on your MongoDB version this is either "fast/easy" or "slow/hard" to process. Compelling reasons to update a version already many years old.
Working example
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/aggtest');
var memberSchema = new Schema({
name: { type: String }
});
var messageSchema = new Schema({
creationDate: { type: Date, default: Date.now },
comment: { type: String },
});
var conversationSchema = new Schema({
members: [ { type: Schema.Types.ObjectId } ],
comments: [messageSchema]
});
var Member = mongoose.model( 'Member', memberSchema );
var Conversation = mongoose.model( 'Conversation', conversationSchema );
async.waterfall(
[
// Clean
function(callback) {
async.each([Member,Conversation],function(model,callback) {
model.remove({},callback);
},
function(err) {
callback(err);
});
},
// add some people
function(callback) {
async.map(["bill","ted","fred"],function(name,callback) {
Member.create({ "name": name },callback);
},callback);
},
// Create a conversation
function(names,callback) {
var conv = new Conversation();
names.forEach(function(el) {
conv.members.push(el._id);
});
conv.save(function(err,conv) {
callback(err,conv,names)
});
},
// add some comments
function(conv,names,callback) {
async.eachSeries(names,function(name,callback) {
Conversation.update(
{ "_id": conv._id },
{ "$push": { "comments": { "comment": name.name } } },
callback
);
},function(err) {
callback(err,names);
});
},
function(names,callback) {
Conversation.findOne({},function(err,conv) {
callback(err,names,conv.comments[1].creationDate);
});
},
function(names,from,callback) {
var ids = names.map(function(el) {
return el._id
});
var pipeline = [
{ "$match": {
"$and": [
{ "members": ids[0] },
{ "members": ids[1] }
]
}},
{ "$project": {
"members": 1,
"comments": {
"$setDifference": [
{ "$map": {
"input": "$comments",
"as": "c",
"in": { "$cond": [
{ "$gte": [ "$$c.creationDate", from ] },
"$$c",
false
]}
}},
[false]
]
}
}}
];
//console.log(JSON.stringify(pipeline, undefined, 2 ));
Conversation.aggregate(
pipeline,
function(err,result) {
if(err) throw err;
console.log(JSON.stringify(result, undefined, 2 ));
callback(err);
}
)
}
],
function(err) {
if (err) throw err;
process.exit();
}
);
Which produces this output:
[
{
"_id": "55a63133dcbf671918b51a93",
"comments": [
{
"comment": "ted",
"_id": "55a63133dcbf671918b51a95",
"creationDate": "2015-07-15T10:08:51.217Z"
},
{
"comment": "fred",
"_id": "55a63133dcbf671918b51a96",
"creationDate": "2015-07-15T10:08:51.220Z"
}
],
"members": [
"55a63133dcbf671918b51a90",
"55a63133dcbf671918b51a91",
"55a63133dcbf671918b51a92"
]
}
]
Note the "comments" only contain the last two entries which are "greater than or equal" to the date which was used as input ( being the date from the second comment ).

How to get a document in mongodb only if at least 3 columns have non null values

I have a collection in MongoDB say STUDENT with attributes id, name, standard, marks, average. Now I want to write a query so that I get only those documents where at least 3 attributes contain non-null values.
All those documents that contain non null value in (name, standard, marks) or (name,marks,average) or (name,standard,marks,average) or (id, name, standard, marks, average) should be printed. But if any document contains only (name,standard) as non null or (standard,marks) should be ignored.
I would have said that given "students" documents like this:
{ name: "a", standard: "b", marks: 10 },
{ name: "b", marks: 5, average: 2 },
{ id: 2, name: "c", marks: 10, average: 7 },
{ name: "c", standard: "b" },
{ standard: "c", marks: 3 }
Then "ideally" you would do something like this:
db.students.find({
"$or": [
{
"$and": [
{ "name": { "$exists": true } },
{ "name": { "$ne": null } },
{ "standard": { "$exists": true } },
{ "standard": { "$ne": null } },
{ "marks": { "$exists": true } },
{ "marks": { "$ne": null } },
],
},
{
"$and": [
{ "name": { "$exists": true } },
{ "name": { "$ne": null } },
{ "marks": { "$exists": true } },
{ "marks": { "$ne": null } },
{ "average": { "$exists": true } },
{ "average": { "$ne": null } }
],
},
{
"$and": [
{ "name": { "$exists": true } },
{ "name": { "$ne": null } },
{ "marks": { "$exists": true } },
{ "marks": { "$ne": null } },
{ "standard": { "$exists": true } },
{ "standard": { "$ne": null } },
{ "average": { "$exists": true } },
{ "average": { "$ne": null } }
],
},
{
"$and": [
{ "id": { "$exists": true } },
{ "id": { "$ne": null } },
{ "name": { "$exists": true } },
{ "name": { "$ne": null } },
{ "marks": { "$exists": true } },
{ "marks": { "$ne": null } },
{ "standard": { "$exists": true } },
{ "standard": { "$ne": null } },
{ "average": { "$exists": true } },
{ "average": { "$ne": null } }
],
}
]
})
Which excludes those last two documents.
Also in modern MongoDB 2.6 and greater versions you get index intersection, or a version of such in 2.4 versions considering the $or operand. So you can index like so:
db.student.ensureIndex({ "name": 1, "standard": 1, "marks": 1 })
db.student.ensureIndex({ "name": 1, "marks": 1, "average": 1 })
db.student.ensureIndex({ "name": 1, "marks": 1, "standard": 1, "average": 1 })
db.student.ensureIndex({ "id": 1, "name": 1, "marks": 1, "standard": 1, "average": 1 })
That can add up to a lot of "index" space usage, so the means may outweigh the ends in this case.
Of course for a more flexible approach to determining this (though not as fast) then you can approach the aggregation framework:
db.students.aggregate([
{ "$project": {
"id": { "$ifNull": [ "$id", null ] },
"name": { "$ifNull": [ "$name", null ] },
"marks": { "$ifNull": [ "$marks", null ] },
"standard": { "$ifNull": [ "$standard", null ] },
"average": { "$ifNull": [ "$average", null ] },
"fields": {
"$add": [
{ "$cond": [ { "$ifNull": [ "$id", null ] }, 1, 0 ] },
{ "$cond": [ { "$ifNull": [ "$name", null ] }, 1, 0 ] },
{ "$cond": [ { "$ifNull": [ "$marks", null ] }, 1, 0 ] },
{ "$cond": [ { "$ifNull": [ "$standard", null ] }, 1, 0 ] },
{ "$cond": [ { "$ifNull": [ "$average", null ] }, 1, 0 ] },
]
}
}},
{ "$match": { "fields": { "$gte": 3 } } }
])
Which is essentially the more "literal" interpretation of your question if limited by the aggregation framework constraint of actually needing to declare all of those "fields" that are possible.
The $ifNull operator is the one doing the "heavy lifting", by replacing "non-existant" or null fields with a null value for evaluation. You could also hopefully "try" to filter with a $match in the initial pipeline stage much as was done in the first query to reduce the input.
The final real catch comes in if you just have **too many* varying field combinations to specify in either form and you just need to know that "three" or more of your fields essentially exist or are not null.
This approach comes down to using $where form of evaluation, which is the least efficient way to handle the general query, but it is the most flexible since the JavaScript code can handle these situations:
db.students.find(
function() {
var count = 0;
for ( var k in this ) {
if ( ( k != null) && ( k != "_id") ) {
count++;
if ( count >= 3 )
break;
}
}
return ( count >= 3 );
}
)
So while the last forms "looks" simple, it is actually pretty horrible as there is no way to avoid what essentially ends up as a "full collection scan", as all the fields in each document get evaluated for the conditions in JavaScript. Well at least until the count of "three" is reached.
That gives you a few approaches. Hopefully the first one actually suits.