Update nested array objects in MongoDB - node.js

I have to deal with objects of the following type in a NodeJS app (using mongodb driver):
data_test = {
"id": "105-20090412",
"date": new Date('2020-09-04T14:00:00.000Z'),
"station": {
"name": "AQ105",
"loc": {
"type": "Point",
"coordinates": [14.324498, 40.821930]
},
"properties": {}
},
"samples": [{
"t": new Date('2020-09-04T14:14:00.000Z'),
"data": {
//"temp_celsius": 31.81,
//"humRelPercent": 39,
"press_mBar": 1021.12,
"PM10": 200
}
}]
}
I receive every 2 minutes data as above.
I want to:
If the data received has an id not yet present on MongoDB do an insert
If the data received has a sample object with a Date (t property) yet present then add properties to this one (for example readings of different sensors)
If the data received has a sample object with a Date (t property) not yet present in samples array, then add this new one
I would like to do what described above with the minor count possible of round-trips to the MongoDB server.
I hope to have been clear enough.
Any suggestion?
Thanks in advance.

Here's my suggestion, this is not the correct answer. You will need to fiddle with the query portion. The query below should work for 1 & 3, for 2 you will have to play around.
db.collection.updateOne(
{ "id" : "105-20090412", "samples.t": <Date> },
{ $push: { "samples" : <sample> } },
{ $setOnInsert: { station: <station> } },
{ upsert: true }
);
References:
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/
https://docs.mongodb.com/manual/reference/operator/update/setOnInsert/#up._S_setOnInsert
https://docs.mongodb.com/manual/reference/operator/update/push/

I finally came to the following solution, perhaps not the most efficient one:
try {
const db = client.db(dbName);
const collection = db.collection(collectionName);
// retrive id, station, date and samplesToAdd as separate objects
let {
id,
...dataToInsert
} = data
//id = new ObjectID(id)
const queryBy_id = {
_id: id
}
// first check if doc exists
let res_query = await collection.findOne(queryBy_id)
// if doc does not exists then insert a new one
if (!res_query) {
res_insert = await collection.insertOne({
_id: id,
...dataToInsert
})
return res_insert;
} else {
// retrive samples from initial query
let current_samples = res_query.samples
// check if sample in dataToInsert yet exists
// use getTime to correctly compare dates
let idx = current_samples.findIndex(x => x.t.getTime() == dataToInsert.samples[0].t.getTime())
if (idx >= 0) {
// find index of sample to update
let current_t = current_samples[idx].t
// merge data yet stored with new one
current_samples.data = {
...current_samples[idx].data,
...dataToInsert.samples[0].data
}
let resUpdateSample = await collection.updateOne({
_id: id,
'samples.t': current_t
}, {
$set: {
'samples.$.data': current_samples.data
}
})
return resUpdateSample
} else {
// add data to samples array
let resAddToSamples = await collection.updateOne({
_id: id
}, {
$push: {
samples: dataToInsert.samples[0]
}
})
return resAddToSamples
}
}
} catch (err) {
logger.error(err);
}
How can I improve it?
Thanks.

Related

Mongoose: cannot infer query fields to set, path 'participants' is matched twice

I'm using mongoose with Node.js to create a document of chat with participants as one of the fields if the chat doesn't exist.
If it does exist then simply increment the status to 1 or any number.
My current Solution:
try {
let query = { participants: { $all: [CURRENT_USER_ID, TARGETED_ID] } };
let update = { $inc: { status: 1 }};
let options = { upsert: true, new: true };
let chat = await Chat.findOneAndUpdate(
query,
update,
options
).exec();
console.log(chat);
} catch (err) {
console.log(err.message);
}
I will receive an error
"cannot infer query fields to set, path 'participants' is matched
twice"
I even use this solution and it doesn't work, it created an empty list of participants instead.
let query = {
participants: {
$all: [
{ $elemMatch: { $eq: CURRENT_USER_ID } },
{ $elemMatch: { $eq: TARGETED_ID } }
]
}
};
Any help would be really helpful. Thanks

Mongoose SUM get stacked

I'm trying to make trivial SUM on mongoDB to count number of prices for single client.
My collection:
{"_id":"5d973c71dd93adfbda4c7272","name":"Faktura2019006","clientId":"5d9c87a6b9676069c8b5e15b","expiration":"2019-10-02T01:11:18.965Z","price":999999,"userId":"123"},
{"_id":"5d9e07e0b9676069c8b5e15d","name":"Faktura2019007","clientId":"5d9c87a6b9676069c8b5e15b","expiration":"2019-10-02T01:11:18.965Z","price":888,"userId":"123"}
What I tried:
// invoice.model.js
const mongoose = require("mongoose");
const InvoiceSchema = mongoose.Schema({
_id: String,
name: String,
client: String,
userId: String,
expiration: Date,
price: Number
});
module.exports = mongoose.model("Invoice", InvoiceSchema, "invoice");
and
// invoice.controller.js
const Invoice = require("../models/invoice.model.js");
exports.income = (req, res) => {
console.log("Counting Income");
Invoice.aggregate([
{
$match: {
userId: "123"
}
},
{
$group: {
total: { $sum: ["$price"] }
}
}
]);
};
What happen:
When I now open a browser and code above is being called, I get console log 'Counting Income' in terminal however in browser it's just loading forever and nothing happen.
Most likely I just miss some stupid minor thing but I'm trying to find it out for quite a long time without any success so any advise is welcome.
The reason that the controller never finishes is because you are not ending the response process (meaning, you need to use the res object and send something back to the caller).
In order to get the aggregate value, you also need to execute the pipeline (see this example).
Also, as someone pointed out in the comments, you need to add _id: null in your group to specify that you are not going to group by any specific field (see the second example here).
Finally, in the $sum operator, for what you're trying to do, you just need to remove the array brackets since you only want to sum on a single field (see a few examples down here).
Here is the modified code:
// invoice.controller.js
const Invoice = require("../models/invoice.model.js");
exports.income = (req, res) => {
console.log("Counting Income");
Invoice.aggregate([
{
$match: {
userId: "123"
}
},
{
$group: {
_id: null,
total: { $sum: "$price" }
}
}
]).then((response) => {
res.json(response);
});
};
Edit for your comment about when an empty array is returned.
If you want to always return the same type of object, I would control that in the controller. I'm not sure if there is a fancy way to do this with the aggregate pipeline in mongo, but this is what I would do.
Invoice.aggregate([
{
$match: {
userId: "123"
}
},
{
$group: {
_id: null,
total: { $sum: "$price" }
}
},
{
$project: {
_id: 0,
total: "$total"
}
}
]).then((response) => {
if (response.length === 0) {
res.json({ total: 0 });
} else {
// always return the first (and only) value
res.json(response[0]);
}
});
Here, if you find a userId of 123, then you would get this as the return:
{
"total": 1000887
}
But if you change the userId to, say, 1123 which doesn't exist in your db, the result will be:
{
"total": 0
}
This way, your client can always consume the same type of object.
Also, the reason I put the $project pipeline stage in there was to suppress the _id field (see here for more info).

Select fields in mongoose query where field value not equal to something

I am basically trying to update a document and then select the fields from the result where the field value is not equal to something. Assume jwt_id to be b816cf00e9f649fbaf613e2ca2d523b5.
Query
const removeDevices = await Identity.findOneAndUpdate(
{
userID: user_id
},
{
$pull: {
activeTokens: {
jti: {
$ne: jwt_id
}
}
}
},
).select(["-_id", "activeTokens.jti"]);
Now, running this query gives the following output:
{ activeTokens:
[ { jti: '5d872359af2c47e5970c1fae531adf0e' },
{ jti: 'd3ac84f520614067b1caad504d7ab27f' },
{ jti: '25c6fa96705c4eec96e1427678c3ff50' },
{ jti: 'b816cf00e9f649fbaf613e2ca2d523b5' }
]
}
How can I get all the jti fields except { jti: b816cf00e9f649fbaf613e2ca2d523b5 } from the select command?
Desired Output
{ activeTokens:
[ { jti: '5d872359af2c47e5970c1fae531adf0e' },
{ jti: 'd3ac84f520614067b1caad504d7ab27f' },
{ jti: '25c6fa96705c4eec96e1427678c3ff50' },
]
}
It's hard to say for certain without testing, but i don't think mongoose returns the document after it was modified, but rather simply returns the matching document. So, i think in the case of findOneAndUpdate, you would have to have your query match to do the pull, and then manually filter the array again in application code to get the desired output.
This might work:
const removeDevices = await Identity.findOneAndUpdate(
{
userID: user_id
},
{
$pull: {
'activeTokens.jti': { $ne: jwt_id }
}
},
).select(["-_id", "activeTokens.jti"]).then(identity=>identity.activeTokens.filter(token=>token.jti!==jwt_id));
If the above doesn't work for some reason, then i would try something more simpler
simple:
const removeDevices = await Identity.findOne({userID: user_id}).select(["-_id", "activeTokens"]).then(identity=>{
const removedTokens = []
identity.activeTokens = identity.activeTokens.filter(token=>{
if(token.jti===jwt_id) {
return true;
}
removedTokens.push(token);
})
identity.save(err=>{
console.log('doc saved')
});
return removedTokens;
});
or (atomic):
const removeDevices = await Identity.findOne({userID: user_id}).select('activeTokens','jti _id').then(identity=>{
const removedTokens = identity.activeTokens.filter(token=>token.jti!==jwt_id);
const result = await Identity.update({userId:user_id},{$pull:{'activeTokens._id': { $in: removedTokens.map(t=>t._id) } }});
console.log(result.nModified);
return removedTokens;
});

How to update a value with aggregate in mongodb and node

I post this question in relation to my use case.
It is true that there is a lot of response in the same subject but I do not find an answer .
that is why I ask you for help Thanks.
I would like to be able to update the lineItemStatus inside lineItems array.
Here is my model :
const orderSchema = new Schema(
lineItems: [{
lineItemStatus: {
type: String,
default: 'en waiting for validation',
lowercase: true
}
}]
)
The result look like this
{
"_id": "5c659cd0be79c124126d5ec2",
"lineItems": [{
"lineItemStatus": "waiting for validation", //the status to update
"_id": "1"
},
{
"lineItemStatus": "delivered",
"_id": "2"
}
]
}
First I'm able to get a single item of lineItems.
this is the code
async updateStatus(req, res) {
let givenLineItemId = req.body.lineItemId
let givenlineItemStatus = req.body.status // the status to update
try {
const ObjectId = mongoose.Types.ObjectId
const aggregationStages = [
{
$unwind: '$lineItems'
},
{
$match: {
'lineItems._id': ObjectId(givenLineItemId)
}
}
]
await Order
.aggregate(aggregationStages)
.exec(function(err, orders) {
if (err) res.status(400).send(err)
res.status(200).send(orders)
})
} catch (err) {
return res.status(500).send(err)
}
}
But now i'm not able to to update the lineItemStatus i see some way to use set or push but it doesn't work.
Thanks a lot for the help.
The aggregation stage itself does not support updates. You have two options:
1) Collect the aggregate results into a variable and do a bulk update. See link.
2) Call forEach on the aggregate. You can see samples provided in this answer.

Find when the keys are unkown in mongodb

How can I getthe data that has email as abc#gmail.com in mongoDB?I don't know the Key Name and I want to iterate through all the data.
I have data like this:
{
"_id":"5c0a1589a5a41b2ae707317b",
"test1":{
"email":"abc#gmail.com",
"phoneNo":"123456897",
"endpointId":"test1"
}
}
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email":"abc#gmail.com",
"phoneNo":"123456897",
"endpointId":"test2"
}
}
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email":"pqr#gmail.com",
"phoneNo":"123456897",
"endpointId":"test3"
}
}
But the object key is not known at the time of searching. I want to iterate through all the data and get matched data that has specific email.
If I know the key name like test1,test2 etc then I can use find({test1:{...}}) but Here I don't know the key value.
So, how can I do that?
You can use below aggregation using $objectToArray in mongodb 3.4 and above
db.collection.aggregate([
{ "$addFields": {
"field": { "$objectToArray": "$$ROOT" }
}},
{ "$match": { "field.v.email": "abc#gmail.com" }},
{ "$project": { "field": 0 }}
])
I am assuming you get the objects in array type.
I made a method named findObject. This method will take the object array and the desired email.
Finally, return the first object, that matched with the email.
const data = [{
"_id":"5c0a1589a5a41b2ae707317b",
"test1":{
"email": "abc#gmail.com",
"phoneNo": "123456897",
"endpointId":"test1"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email": "abc#gmail.com",
"phoneNo": "123456897",
"endpointId":"test2"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email": "pqr#gmail.com",
"phoneNo": "123456897",
"endpointId": "test3"
}
}];
const findObject = (data, email) => {
for (let index=0; index<data.length; index++) {
const currentData = data[index];
for (let property in currentData) {
if (property != '_id' && currentData[property].email == email) {
return currentData;
}
}
}
return null;
}
let desiredObject;
const desiredEmail = 'abc#gmail.com';
desiredObject = findObject(data, desiredEmail);
console.log(desiredObject);
And the output will be
{ _id: '5c0a1589a5a41b2ae707317b',
test1:
{ email: 'abc#gmail.com',
phoneNo: '123456897',
endpointId: 'test1' } }
I think you can't do query on totally unknown field! if you could change your schema see here for more info, also you could write script to migrate to a new DB with new schema:
// new doc instance
{
"_id":"5c0a1589a5a41b2ae707317b",
"obj": {
"name": "test1"
"email":"abc#gmail.com"
"phoneNo":"123456897",
"endpointId":"test1"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"obj": {
"name": "test2"
"email":"abc#gmail.com"
"phoneNo":"123456897",
"endpointId":"test2"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"obj": {
"name": "test3"
"email":"pqr#gmail.com"
"phoneNo":"123456897",
"endpointId":"test3"
}
}
otherwise, check this may works correctly. if all of them is not effective so make a query to get all of your data as an Array and use filter method on it:
Model.find({}, (err, docs) => {
const result = docs.filter((doc) => {
for (key in doc) {
if (doc[key].email === 'abc#gmail.com')
return doc;
}
});
console.log(result);
});

Resources