Mongoose bulkwrite update - how to push new obj into an array? - node.js

I have a model that looks like:
fname: String,
lname: String,
rating: [{
rating: {
type: Number,
enum: RATING,
default: 5
},
date: {
type: Date,
default: Date.now
}
}]
I need to perform updates on this Model by adding new object inside the rating array, with new ratings and dates. I would like to use the bulkwrite method on Model.collection to do this because I need to enable bulk updates so that I don't have to update them one by one.
I created an array bulkUpdateOperations = [] and did the following in a loop:
bulkUpdateOperations.push({
'updateOne': {
'filter': {'_id': item.id},
'update': {$push: {rating: {'rating': item.rating, 'date': Date.now}}}
}
});
Person.collection.bulkWrite(bulkUpdateOperations, {orderd: true, w: 1}, callbackfunc);
But nothing gets updated. I get the following response:
...
...
...
insertedCount: 0,
matchedCount: 0,
modifiedCount: 0,
deletedCount: 0,
upsertedCount: 0,
upsertedIds: {},
insertedIds: {},
n: 0 }
I would be very thankful if someone helped me through this problem.
EDIT
Here is the array I'm sending as POST body to update the records:
[{
"id": "5b7d4d348151700014d25bdd",
"rating": 1
},{
"id": "5b771d10c1e03e1e78b854c2",
"rating": 1
},{
"id": "5b771d7ac1e03e1e78b854c8",
"rating": 1
},{
"id": "5b7bd75a33f88c1af8585be0",
"rating": 1
},{
"id": "5b814a2322236100142ac9f6",
"rating": 1
}]
And here is a sample collection in the DB
{
"_id": {
"$oid": "5b7d4d348151700014d25bdd"
},
"status": "ACTIVE",
"fname": "mr. client",
"lname": "good client",
"contact_info": {
"_id": {
"$oid": "5b7d4d348151700014d25bde"
},
"mobile_no": "0011223344",
"phone_no": "11223344"
},
"handlers": [
{
"_id": {
"$oid": "5b7d4d348151700014d25bdf"
},
"date": {
"$date": "2018-08-22T11:47:00.544Z"
},
"id": {
"$oid": "5b7d45fbfb6fc007d8bdc1f4"
}
}
],
"onboarding_date": {
"$date": "2018-08-22T11:47:00.551Z"
},
"rating": [
{
"rating": 5,
"_id": {
"$oid": "5b814a8e22236100142ac9fc"
},
"date": {
"$date": "2018-08-25T12:22:59.584Z"
}
},
{
"rating": 3,
"_id": {
"$oid": "5b814a8e22236100142ac9fb"
},
"date": {
"$date": "2018-08-25T12:24:46.368Z"
}
}
],
"__v": 0
}
EDIT
Adding upsert: true as a filter for updateOne creates a new document with only rating as its value.
SOLUTION
replace
'filter': {'_id': item.id},
by
'filter': {'_id': mongoose.Types.ObjectId(item.id)},

changing
bulkUpdateOperations.push({
'updateOne': {
'filter': {'_id': item.id},
'update': {$push: {rating: {'rating': item.rating, 'date': Date.now}}}
}
});
to
bulkUpdateOperations.push({
'updateOne': {
'filter': {'_id': mongoose.Types.ObjectId(item.id)},
'update': {$push: {rating: {'rating': item.rating, 'date': Date.now}}}
}
});
worked. Notice the type cast I had to manually perform in
'filter': {'_id': mongoose.Types.ObjectId(item.id)},
I thought mongoose would automatically cast the string to an ObjectId type, but maybe because I'm dropping down a level of abstraction by using Person.collection, mongoose did not auto-cast the itemId.
Please feel free to update this answer if anyone can confirm why I had to cast the string manually.

Related

Mongoose aggregate by date and values

I cannot solve a problem I am having on a project.
First of all, I have a Schema called Emotions, shown below:
import mongoose from 'mongoose';
const EmotionSchema = new mongoose.Schema({
classification: {
type: String,
required: true,
},
probability: {
type: Number,
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
}
}, {
timestamps: true,
});
export default mongoose.model('Emotion', EmotionSchema);
And some data like this:
[
{
"_id": "61144a393c532f066725bd24",
"classification": "Feliz",
"probability": 0.98,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-11T22:07:53.331Z",
"updatedAt": "2021-08-11T22:07:53.331Z",
"__v": 0
},
{
"_id": "61144a46d30bd006fa2be702",
"classification": "Feliz",
"probability": 0.98,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-11T22:08:06.618Z",
"updatedAt": "2021-08-11T22:08:06.618Z",
"__v": 0
},
{
"_id": "611541dd62a7f214afab3ceb",
"classification": "Triste",
"probability": 0.9,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:44:29.150Z",
"updatedAt": "2021-08-12T15:44:29.150Z",
"__v": 0
},
{
"_id": "611541f762a7f214afab3cf5",
"classification": "Raiva",
"probability": 0.86,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:44:55.909Z",
"updatedAt": "2021-08-12T15:44:55.909Z",
"__v": 0
},
{
"_id": "6115420362a7f214afab3cf7",
"classification": "Neutro",
"probability": 0.99,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:45:07.297Z",
"updatedAt": "2021-08-12T15:45:07.297Z",
"__v": 0
},
{
"_id": "6115420462a7f214afab3cf9",
"classification": "Neutro",
"probability": 0.99,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:45:08.002Z",
"updatedAt": "2021-08-12T15:45:08.002Z",
"__v": 0
},
{
"_id": "611543d252be9a187c380e5d",
"classification": "Feliz",
"probability": 0.91,
"user": "60eecfeba0810013e750cdbc",
"createdAt": "2021-08-12T15:52:50.599Z",
"updatedAt": "2021-08-12T15:52:50.599Z",
"__v": 0
}
]
What I want is:
First group each of these records by day.
Afterwards, count the values ​​by classification, but separating these classifications.
Something like that:
[
{
"_id": "12/08/2021",
"feliz": 1,
"triste": 1,
"raiva": 1,
"neutro": 2,
"surpreso": 0,
},
{
"_id": "11/08/2021",
"feliz": 2,
"triste": 0,
"raiva": 0,
"neutro": 0,
"surpreso": 0,
}
]
I did something like that, but it's only working for a value, eg "Feliz".
In code:
Emotion.aggregate([
{ $match:
{
user: user._id,
classification: "Feliz"
}
},
{ $group: {
_id : { $dateToString: { format: "%d/%m/%Y", date: "$createdAt" } },
feliz: { $sum: 1 }
}},
], function(err, results) {
if (err) throw err;
return res.json(results);
})
And it returns:
[
{
"_id": "12/08/2021",
"feliz": 1
},
{
"_id": "11/08/2021",
"feliz": 4
}
]
One more question:
From the Client-side, I always get values ​​for "classification" like:
"Feliz", "Triste", "Surpreso", "Raiva", "Neutro".
So is it better to add an enum to my schema "Emotion"?
(Sorry for my English, i hope you understand).
$group by createdAt date and classification, count sum
$group by createdAt only and construct the array of classification and count in key-value pair
$arrayToObject to convert above key-value pair array of object to an object format
Emotion.aggregate([
{
$match: {
user: user._id,
classification: "Feliz"
}
},
{
$group: {
_id: {
createdAt: {
$dateToString: {
format: "%d/%m/%Y",
date: "$createdAt"
}
},
classification: "$classification"
},
count: { $sum: 1 }
}
},
{
$group: {
_id: "$_id.createdAt",
classifications: {
$push: {
k: "$_id.classification",
v: "$count"
}
}
}
},
{
$addFields: {
classifications: {
$arrayToObject: "$classifications"
}
}
}
],
function(err, results) {
if (err) throw err;
return res.json(results);
})
Playground
Note: this will not return classification when count is 0! You need to do this after query in js.
One more question: From the Client-side, I always get values ​​for "classification" like: "Feliz", "Triste", "Surpreso", "Raiva", "Neutro". So is it better to add an enum to my schema "Emotion"?
It is up to your project requirement, you can set these values in enum to restrict the input.

Mongoose aggregation pipeline returns different result each time

I have an aggregation pipeline which supposed to return an ordered array. However. Every time I make an API cal, it returns a different order each time. Here is my code
exports.getResultToCompetition = asyncHandler(async (req, res, next) => {
const data = await Result.aggregate([
{
$match: { competition: mongoose.Types.ObjectId(req.params.competitionId) },
},
{
$project: {
points: 1,
kilogramms: 1,
sector: 1,
user: 1,
competition: 1,
place: 1,
},
},
{
$sort: {
kilogramms: -1,
},
},
{
$lookup: {
from: "users", // must be the PHYSICAL name of the collection
localField: "user",
foreignField: "_id",
as: "user",
},
},
{
$addFields: {
user: {
$arrayElemAt: ["$user", 0],
},
},
},
{
$group: {
_id: "$sector",
res: {
$push: "$$ROOT",
},
},
},
]);
res.status(200).json({ success: true, data });
});
And the Schema:
const mongoose = require('mongoose');
const ResultSchema = new mongoose.Schema({
competitionSeries: {
type: String,
default: 0,
},
points: {
type: Number,
default: 0,
},
sector: {
type: String,
},
kilogramms: {
type: Number,
default: 0,
},
place: {
type: Number,
default: 0,
},
isCompetitionRunning: {
type: Boolean,
default: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
team: {
name: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
partner: {
type: String,
},
teamName: {
type: String,
},
},
competition: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Competition',
required: true,
},
});
module.exports = Result = mongoose.model('Result', ResultSchema);
The kilograms field is saved as a Double in the MongoDB database
I set the value in the frontend in React as a number.
I am not sure if its causing the problem.
The kilograms value supposed to be a float number like 14.5
This is the result which is correct:
{
"success": true,
"data": [
{
"_id": "B",
"res": [
{
"_id": "5eca12e2607c2c0017c16904",
"points": 1,
"kilogramms": 90.52,
"place": 59,
"sector": "B",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb82c89e5d6cc0017e71a2a",
"avatar": "placeholder.jpg",
"role": "user"
}
},
{
"_id": "5eca1406607c2c0017c16918",
"points": 2,
"kilogramms": 39.45,
"place": 10,
"sector": "B",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb83175e5d6cc0017e71a42",
"avatar": "653571a228226499806e8a717cf2d6e1",
"role": "user"
}
},
{
"_id": "5eca11fc607c2c0017c168f5",
"points": 3,
"kilogramms": 29.62,
"place": 8,
"sector": "B",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb83970e5d6cc0017e71a64",
"avatar": "1589316331809-feederuser",
"role": "user"
}
},
{
"_id": "5eca1250607c2c0017c168fa",
"points": 4,
"kilogramms": 24.25,
"place": 56,
"sector": "B",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb83a00e5d6cc0017e71a67",
"avatar": "placeholder.jpg",
"role": "user"
}
}
]
},
{
"_id": "C",
"res": [
{
"_id": "5eca12cc607c2c0017c16903",
"points": 1,
"kilogramms": 72.43,
"place": 52,
"sector": "C",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb83c2ee5d6cc0017e71a70",
"avatar": "56065850e67a64878e5034d06cecc0ea",
"role": "user"
}
},
{
"_id": "5eca1307607c2c0017c16907",
"points": 2,
"kilogramms": 43.35,
"place": 12,
"sector": "C",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb998864d62090017231ce6",
"avatar": "placeholder.jpg",
"role": "user",
"name": "Halász Tibor"
}
},
{
"_id": "5eca1325607c2c0017c16909",
"points": 3,
"kilogramms": 39.43,
"place": 15,
"sector": "C",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb9b46c4d62090017231cf8",
"avatar": "placeholder.jpg",
"role": "user"
}
},
{
"_id": "5eca14c1607c2c0017c16927",
"points": 4,
"kilogramms": 36.99,
"place": 11,
"sector": "C",
"competition": "5eb90caa7b45620017a4d919",
"user": {
"_id": "5eb82fdbe5d6cc0017e71a3d",
"avatar": "placeholder.jpg",
"role": "user"
}
}
]
}
]
}
After I call again the endpoint it shows a different order. Meaning, Sector C is coming first which is not correct because in Sector B there is a higher kilograms result.
You can add a maxKilogramm field to your documents, and sort by that field by adding the following stages to the end of your aggregation.
{
$addFields: {
"maxKilogramm": {
$max: "$res.kilogramms"
}
}
},
{
$sort: {
maxKilogramm: -1,
},
},
{
$project: {
maxKilogramm: 0
}
}
Playground

MongoDB group by ID and then by date

I have a collection in my MongoDB database that stores durations for people who are in groups, it looks a like this:
[{
"_id": "5c378eecd11e570240a9b0ac",
"state": "DRAFT",
"groupId": "5c378eebd11e570240a9ae49",
"personId": "5c378eebd11e570240a9aee1",
"date": "2019-01-07T00:00:00.000Z",
"duration": 480,
"__v": 0
},
{
"_id": "5c378eecd11e570240a9b0bb",
"state": "DRAFT",
"groupId": "5c378eebd11e570240a9ae58",
"personId": "5c378eebd11e570240a9aeac",
"date": "2019-01-07T00:00:00.000Z",
"duration": 480,
"__v": 0
},
{
"_id": "5c378eecd11e570240a9b0c5",
"state": "DRAFT",
"groupId": "5c378eebd11e570240a9ae3e",
"personId": "5c378eebd11e570240a9aef6",
"date": "2019-01-07T00:00:00.000Z",
"duration": 480,
"__v": 0
}]
I would like to be able to run an aggregate query which returns a collection of personIds and the duration grouped per day with the corresponding groupId, which would look like this:
[{
"personId": "5c378eebd11e570240a9aee1",
"time": [{
"date": "2019-01-07T00:00:00.000Z",
"entries": [{
"groupId": "5c378eebd11e570240a9ae49",
"duration": 480,
"state": "DRAFT"
}]
}]
}, {
"personId": "5c378eebd11e570240a9aeac",
"time": [{
"date": "2019-01-07T00:00:00.000Z",
"entries": [{
"groupId": "5c378eebd11e570240a9ae58",
"duration": 480,
"state": "DRAFT"
}]
}]
}, {
"personId": "5c378eebd11e570240a9aef6",
"time": [{
"date": "2019-01-07T00:00:00.000Z",
"entries": [{
"groupId": "5c378eebd11e570240a9ae3e",
"duration": 480,
"state": "DRAFT"
}]
}]
}]
So far, I have written the following aggregation (I'm using Mongoose, hence the syntax):
Time.aggregate()
.match({ date: { $gte: new Date(start), $lte: new Date(end) } })
.group({
_id: '$personId',
time: { $push: { date: '$date', duration: '$duration', state: '$state' } },
})
.project({ _id: false, personId: '$_id', time: '$time' })
Which returns the following:
[{
"personId": "5c378eebd11e570240a9aed1",
"time": [{
"date": "2019-01-11T00:00:00.000Z",
"duration": 480,
"state": "DRAFT"
}, {
"date": "2019-01-11T00:00:00.000Z",
"duration": 480,
"state": "DRAFT"
}
// ...
}]
Hopefully you can see that the durations are being grouped by personId but I've not been able to figure out how to apply another grouping to the time array as the dates are duplicated if a personId has more than one duration for a given date.
Is it possible to group by and ID, push to an array and then group the values in that array as an aggregation or will my application need to map/reduce the results instead?
I would suggest running two $group operations in a row:
db.time.aggregate({
// first, group all entries by personId and date
$group: {
_id: {
personId: "$personId",
date: "$date"
},
entries: {
$push: {
groupId: "$groupId",
duration: "$duration",
state: "$state"
}
}
}
}, {
// then, group previously aggregated entries by personId
$group: {
_id: "$_id.personId",
time: {
$push: {
date: "$_id.date",
entries: "$entries"
}
}
}
}, {
// finally, rename _id to personId
$project: {
_id: 0,
personId: "$_id",
time: "$time"
}
})
In Mongoose it should be something like that:
Time.aggregate()
.match({
date: {
$gte: new Date(start),
$lte: new Date(end)
}
})
.group({
_id: {
personId: '$personId',
date: '$date'
},
entries: {
$push: {
groupId: '$groupId',
duration: '$duration',
state: '$state'
}
}
})
.group({
_id: '$_id.personId',
time: {
$push: {
date: '$_id.date',
entries: '$entries'
}
}
})
.project({
_id: false,
personId: '$_id',
time: '$time'
})
db.getCollection("dummyCollection").aggregate(
[
{
"$group" : {
"_id" : "$personId",
"time" : {
"$push" : {
"date" : "$date",
"duration" : "$duration",
"state" : "$state"
}
}
}
},
{
"$project" : {
"_id" : false,
"personId" : "$_id",
"time" : "$time"
}
},
{
"$unwind" : "$time"
},
{
"$group" : {
"_id" : "$time.date",
"time" : {
"$addToSet" : "$time"
}
}
}
]
);
Use $addToSet which returns an array of all unique values that results from applying an expression to each document in a group of documents that share the same group by key.

Issue with Mongoose aggregation

My requirement is for a particular user get all the expenses which are grouped based on the month and return the Month and the total expenses for that month.
I have collection something like below.
expenses = {{
"_id": {
"$oid": "5bc0f74df46dae0bf4ffdc39"
},
"user": {
"$oid": "5bab847a5b0d2e2ce8b4cbe5"
},
"date": "15-Mar-2018",
"expenseInfo": "Clothes",
"category": "Shopping",
"amount": 100,
"__v": 0
},
{
"_id": {
"$oid": "5bc0f78bf46dae0bf4ffdc3b"
},
"user": {
"$oid": "5bab847a5b0d2e2ce8b4cbe5"
},
"date": "11-Apr-2018",
"expenseInfo": "mobile",
"category": "Bills",
"amount": 100,
"__v": 0
},
{
"_id": {
"$oid": "5bc0f76cf46dae0bf4ffdc3a"
},
"user": {
"$oid": "5bab847a5b0d2e2ce8b4cbe5"
},
"date": "18-Apr-2018",
"expenseInfo": "Petrol",
"category": "Fuel",
"amount": 20,
"__v": 0
}
}
Here is my Expense schema.
Also, Note that user object(user is a different schema which is referred in Expense schema)
const ExpenseSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
},
date: {
type: String
},
expenseInfo: {
type: String,
required: true
},
category: {
type: String,
required: true
},
amount: {
type: Number,
required: true
}
});
I tried something like this, but I am not getting any data. What is the issue here?
Expense.aggregate([{
$match: {
"_id": request.user.id
}
},
{
"$group": {
"_id": {
"$arrayElemAt": [{
"$split": ["$date", "-"]
}, 1]
},
"Total": {
"$sum": "$amount"
}
}
}
])

How filter array of objects elements mongoose?

I have the following schema:
User: {
age: Number,
name: String,
comments: {
added: Date,
body: String
}
}
I need get all users, with comments added greater than a custom date.
Example data:
[{
"name": "John",
"age: 21,
"comments": [{
"added": 21-01-12,
"body": "blabla1"
}, {
"added": 21-02-12,
"body": "blabla2"
}, {
"added": 21-01-10,
"body": "blabla3"
}]
}, {
"name": "Bruno",
"age: 33,
"comments": [{
"added": 21-01-10,
"body": "ololo1"
}, {
"added": 21-02-12,
"body": "ololo2"
}, {
"added": 21-01-09,
"body": "ololo3"
}]
}]
I need all users with all comments greater than 01-01-11 without comments less than this date.
Expected result:
{
"name": "John",
"age: 21,
"comments": [{
"added": 21-01-12,
"body": "blabla1"
}, {
"added": 21-02-12,
"body": "blabla2"
}]
}, {
"name": "Bruno",
"age: 33,
"comments": [{
"added": 21-02-12,
"body": "ololo2"
}]
}
How can I go about this ?
const findQuery = [
{$unwind: "$comments" },
{$match: {"comments.added": {$gt: lastUpdate} } },
{$group: {_id: "$_id", comments: {$push: "$comments"} } }
];
User.aggregate(findQuery)
To filter a date field in Mongoose (and mongo), you need to use $gte, $gt, $lt, $lte. In your specific example, you need $gte (greater than or equal)
So, if your model is User
User.find({comments.added : {$gte : new Date(2011, 1, 1)}}, function(err, comments) {
});
Except if there are some reasons, I may propose you to prefer ISODate that are more accurate, and you do the same to filter entries
mph

Resources