How can I implement Auto calculated field in mongodb? - node.js

I have a collection of workorders where it have time_started and time_completed values. I want to have auto-calculated field called duration that automatically calculated time_completed - time_started. What is the best way?
Essentially what I want is, when App post requests with a completed time, my duration is auto calculated.
Example data
router.post('/completed', function (req, res) {
const time_completed = req.body.time_completed
const workorder_id = req.body.workorder_id
db.collection(workorder).updateOne(
{ _id: ObjectId(workorder_id) },
{
$set: {
time_completed: time_completed,
}
},
function (err, result) {
if (err) throw err
res.send('Updated')
}
)
});

Query
pipeline update requires >= MongoDB 4.2
add the time_completed
add the duration also
*replace the 6 with the javascript variable that holds the time_completed Date
*duration will be in milliseconds
Test code here
db.collection.update(
{"_id": 1},
[
{
"$set": {
"time_completed": 6,
"duration": {
"$subtract": [
6,
"$time_started"
]
}
}
}
])
Edit
You have strings on your database, i thought it was dates, best thing to do is to convert all those string-dates to Date with $dataFromString like the code bellow, and use the first query.
To get the string if you needed from Date you can do $stringFromDate when you need it.
Query
same like above but it converts string dates to Date to do the substraction (keeps the dates in strings inside the database)
Test code here
db.collection.update({
"_id": 1
},
[
{
"$set": {
"time_completed": "2021-11-21T00:00:00.000Z",
"duration": {
"$subtract": [
ISODate("2021-11-21T00:00:00Z"),
{
"$dateFromString": {
"dateString": "$time_started"
}
}
]
}
}
}
])

Related

MongoDB Array of ID's to compare and pull a value

I'm a little newer to using MongoDB and NoSQL for my stack. ULTIMATELY, I'm switching to NoSQL for the fact of JSON parsed data already. I can accomplish what I want in MySQL - but I'm also attempting to teach myself something new.
Currently, I have no problem getting connected and setting up my Schemas for NodeJS. I have a document within MongoDB that returns what my customers pay on fuel according to National Averages - our customers give us ranges for us to be able to find what specific dollar amount they are paying.
My MongoDB Document looks as the following :
main_database
|- customerFSC (name of document)
|--
{
"_id":{"$oid":"5e5ecc04e8da861114079ab2"},
"custID":"01",
"custName":"Customer ABC",
"avgLow":["1.19","1.24","1.29","1.34","1.39","1.44","1.49","1.54","1.59","1.64","1.69","1.74","1.79","1.84","1.89","1.94","1.99"],
"avgHigh":["1.239","1.289","1.339","1.389","1.439","1.489","1.539","1.589","1.639","1.689","1.739","1.789","1.839","1.889","1.939","1.989","2.039"],
"custFscPM":["0.01","0.02","0.03","0.04","0.05","0.06","0.07","0.08","0.09","0.10","0.11","0.12","0.13","0.14","0.15","0.16","0.17"]
}
If fuel average for the week is at 1.215 - it would be ranged between the avgLow of 1.19 and the avgHigh of 1.239 but return the actual pay of 0.01 in custFscPM
My MongoDB Mongoose Node code is as follows
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const fscCustSchema = new Schema({
custID: String,
custName: String,
avgLow: Array,
avgHigh: Array,
custFscPM: Array
},
{ collection: 'customerFSC' }
);
module.exports = mongoose.model('customerFSC', fscCustSchema);
fscCustModel.find( { $and: [
{ $gte: [ { avgLow: USAFscAVG } ] },
{ $lte: [ { avgHigh: USAFscAVG } ] }
]},function(err2,resp2) {
if (err2) console.log("Error Thrown Looking up Fuel:" + err2);
console.log(resp);
});
You can use aggregation which is way to powerful way of querying MongoDB than using .find(), Try below query :
db.collection.aggregate([
{
$project: {
custFscPM: {
$arrayElemAt: [
"$custFscPM",
{
$indexOfArray: [
"$avgLow",
{
$arrayElemAt: [
{
$filter: {
input: "$avgLow",
cond: {
$gte: [
{
$toDouble: "$$this"
},
1.94 // Your input
]
}
}
},
0
]
}
]
}
]
}
}
}
])
Looking at your data, in this query we're finding the element in avgLow array which is greater than or equal to passed in value & getting it's index & find the element from custFscPM at the same index would resolve your requirement.
Note : As your values in avgLow are strings we need to convert those to double in order to do comparison.
Test : MongoDB-Playground

Query Mongodb/Mongoose for Field with array of dates containing dates after a certain date

My question is an extension of this one MongoDB/Mongoose querying at a specific date?
I have a model Tournament. They will have games on multiple dates. I want to query for tournaments that have games on a certain date. I tried to used the method described in the post.
Example data
{
"_id": {
"$oid": "59a70655b300012a044b2a48"
},
...,
"gameDates": [
{
"$date": "2017-09-01T05:59:59.000Z"
},
{
"$date": "2017-09-02T05:59:59.000Z"
}
]
}
Using mlab, this returns nothing. If I query for specific $date, not a range, it works in mlab, not mongoose.
{
"gameDates": {
"$date": {
"$lt": "2017-09-02T00:00:00.000Z"
}
}
}
Update: In node,
var conditions={gameDates: { $date: '2017-09-01T05:59:59.000Z' } };
var results='gameDates';
Tournament.find(conditions, results).exec(
function(err, tournament) {
if (err) {
console.log("err"+err);
}
console.log("tournament",tournament);
});
Gives me
errError: Can't use $date with Array.

$push and $set same sub-document in an Array

I'm trying to keep an history of states in a subdocument array with mongoosejs 4.9.5 and mongo 3.2.7
Example of document structure:
company (Schema)
employees (Schema): [ ]
currentState: String
states (Schema): [ ]
state: String
starts: Date
ends: Date
When I change the employee state, I want to change the currentState, add the new state into the states array, and update the last state for define the 'ends' timestamp
// I get the last state position from a previous find request
var lastStateIndex = employee.stateHistory.length - 1;
var changeStateDate = new Date();
// Prepare the update
var query = { _id: companyId, "employees._id": employeeId };
var update = {
$set: {
"employees.$.state": newState,
`employees.$.stateHistory.${lastStateIndex}.ends`: changeStateDate
},
$push: {
"employees.$.stateHistory": {
state: newState,
starts: changeStateDate
}
}
}
Company.findOneAndUpdate(query, update, { multi:false, new:true}, ... )
Mongo is returning the following error
{"name":"MongoError","message":"Cannot update 'employees.0.stateHistory.0.ends' and 'employees.0.stateHistory' at the same time","ok":0,"errmsg":"Cannot update 'employees.0.stateHistory.0.ends' and 'employees.0.stateHistory' at the same time","code":16837}
Any suggestions how to avoid running two updates for that purpose?
Any work around for avoid storing the 'ends' date, but being able to calculate it after based on the 'starts' of the next item in the array?
Thank you,
I expected this to already be answered elsewhere, but no other reasonable response seems to exist. As commented, you cannot actually do this in a single update operation because the operations "conflict" on the same path. But .bulkWrite() allows "multiple updates" to be applied in a single request and response.
Company.bulkWrite([
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$set": {
"employees.$.state": newState,
[`employees.$.stateHistory.${lastStateIndex}.ends`]: changeStateDate
}
}},
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$push": {
"employees.$.stateHistory": {
"state": newState,
"starts": changeStateDate
}
}
}
}}
])
Now of course .bulkWrite() does not return the "modified document" like .findOneAndUpdate() does. So if you need to actually return the document, then you need to add to the Promise chain instead:
Company.bulkWrite([
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$set": {
"employees.$.state": newState,
[`employees.$.stateHistory.${lastStateIndex}.ends`]: changeStateDate
}
}},
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$push": {
"employees.$.stateHistory": {
"state": newState,
"starts": changeStateDate
}
}
}
}}
]).then( result => {
// maybe inspect the result
return Company.findById(companyId);
})
Of course noting that it is "possible" that another modification can be made to the document in between when the .bulkWrite() is applied and the .findById() is executed. But that is the cost of the operation you are doing.
It is generally best to consider if you actually need the returned document or not. In most instances you simply already have the information and any "updates" you should be aware of because you are "issuing them", and if you want "truly reactive" then you should be listening for other change events on the data through a socket instead.
Note you could simply "chain" the "multiple" .findOneAndUpdate() calls, but this is indeed "multiple" calls and responses from the server, as opposed to the one using .bulkWrite(). So there really isn't anything to gain by doing otherwise.

Mongoose multi update

I want to update multiple docs with different values.
My Database looks something like this.
[
{
"_id": 1,
"value": 50
},
{
"_id": 2,
"value": 100
}
]
This Query return an error because i'm passing an array instead of an object in the $set.
Model.update({_id: {$in: ids}}, {$set: ids.value}, {multi: true};
I want my database to look like this
[
{
"_id": 1,
"value": 4
},
{
"_id": 2,
"value": 27
}
]
Supposing you had an array of objects that you wanted to update in your collection on matching ids like
var soldItems = [
{
"_id": 1,
"value": 4
},
{
"_id": 2,
"value": 27
}
];
then you could use the forEach() method on the array to iterate it and update your collection:
soldItems.forEach(function(item)){
Model.update({"_id": item._id}, {"$set": {"value": item.value }}, callback);
});
or use promises as
var updates = [];
soldItems.forEach(function(item)){
var updatePromise = Model.update({"_id": item._id}, {"$set": {"value": item.value }});
updates.push(updatePromise);
});
Promise.all(updates).then(function(results){
console.log(results);
});
or using map()
var updates = soldItems.map(function(item)){
return Model.update({"_id": item._id}, {"$set": {"value": item.value }});
});
Promise.all(updates).then(function(results){
console.log(results);
});
For larger arrays, you could take advantage of using a bulk write API for better performance. For Mongoose versions >=4.3.0 which support MongoDB Server 3.2.x,
you can use bulkWrite() for updates. The following example shows how you can go about this:
var bulkUpdateCallback = function(err, r){
console.log(r.matchedCount);
console.log(r.modifiedCount);
}
// Initialise the bulk operations array
var bulkOps = soldItems.map(function (item) {
return {
"updateOne": {
"filter": { "_id": item._id } ,
"update": { "$set": { "value": item.value } }
}
}
});
// Get the underlying collection via the native node.js driver collection object
Model.collection.bulkWrite(bulkOps, { "ordered": true, w: 1 }, bulkUpdateCallback);
For Mongoose versions ~3.8.8, ~3.8.22, 4.x which support MongoDB Server >=2.6.x, you could use the Bulk API as follows
var bulk = Model.collection.initializeOrderedBulkOp(),
counter = 0;
soldItems.forEach(function(item) {
bulk.find({ "_id": item._id }).updateOne({
"$set": { "value": item.value }
});
counter++;
if (counter % 500 == 0) {
bulk.execute(function(err, r) {
// do something with the result
bulk = Model.collection.initializeOrderedBulkOp();
counter = 0;
});
}
});
// Catch any docs in the queue under or over the 500's
if (counter > 0) {
bulk.execute(function(err,result) {
// do something with the result here
});
}
First of all your update() query is not ok..
See documentation for this here
Model.update(conditions, update, options, callback);
Suppose your current db model contains docs like this (as you described in question as well):
[
{
"_id": 1,
"value": 50
},
{
"_id": 2,
"value": 100
}
];
and you've below array which contains objects (i.e., docs) to be modified with current db's docs to like this:
idsArray: [
{
"_id": 1,
"value": 4
},
{
"_id": 2,
"value": 27
}
];
From my experience with mongodb & mongoose, i don't think you can update all docs with single line query (that's you're trying to do).. (P.S. I don't know about that so I am not sure to this..)
But to make your code work, you will be doing something like this:
Idea: Loop over each doc in docs i.e, idsArray and call update() over it..
So, Here's code to this:
idsArray.forEach(function(obj) {
Model.update({_id: obj._id}, {$set: {value: obj.value}});
});
In above code, I am supposing you've _id values in db docs as they 're written above (i.e, "_id": 1).. But if they're like this "_id": ObjectId('1')
[
{
"_id": ObjectId('1'),
"value": 50
},
.....
.....
]
then you'll be need to convert _id to ObjectId(obj._id) in update() query..
so for that you'll be doing like this.
var ObjectId = require('mongodb').ObjectID;
idsArray.forEach(function(obj) {
Model.update({_id: ObjectId(obj._id)}, {$set: {value: obj.value}});
});
P.S. Just confirm it (i.e., _id) before go forward..
Hope this helps.
Cheers,
Multi update can be used only for updating multiple documents to the same value(s) or updating with the same update operation for all documents.
If you want to update to different values you have to use several update statements.

how to combine array of object result in mongodb

how can i combine match document's subdocument together as one and return it as an array of object ? i have tried $group but don't seem to work.
my query ( this return array of object in this case there are two )
User.find({
'business_details.business_location': {
$near: coords,
$maxDistance: maxDistance
},
'deal_details.deals_expired_date': {
$gte: new Date()
}
}, {
'deal_details': 1
}).limit(limit).exec(function(err, locations) {
if (err) {
return res.status(500).json(err)
}
console.log(locations)
the console.log(locations) result
// give me the result below
[{
_id: 55 c0b8c62fd875a93c8ff7ea, // first document
deal_details: [{
deals_location: '101.6833,3.1333',
deals_price: 12.12 // 1st deal
}, {
deals_location: '101.6833,3.1333',
deals_price: 34.3 // 2nd deal
}],
business_details: {}
}, {
_id: 55 a79898e0268bc40e62cd3a, // second document
deal_details: [{
deals_location: '101.6833,3.1333',
deals_price: 12.12 // 3rd deal
}, {
deals_location: '101.6833,3.1333',
deals_price: 34.78 // 4th deal
}, {
deals_location: '101.6833,3.1333',
deals_price: 34.32 // 5th deal
}],
business_details: {}
}]
what i wanted to do is to combine these both deal_details field together and return it as an array of object. It will contain 5 deals in one array of object instead of two separated array of objects.
i have try to do it in my backend (nodejs) by using concat or push, however when there's more than 2 match document i'm having problem to concat them together, is there any way to combine all match documents and return it as one ? like what i mentioned above ?
What you are probably missing here is the $unwind pipeline stage, which is what you typically use to "de-normalize" array content, particularly when your grouping operation intends to work across documents in your query result:
User.aggregate(
[
// Your basic query conditions
{ "$match": {
"business_details.business_location": {
"$near": coords,
"$maxDistance": maxDistance
},
"deal_details.deals_expired_date": {
"$gte": new Date()
}},
// Limit query results here
{ "$limit": limit },
// Unwind the array
{ "$unwind": "$deal_details" },
// Group on the common location
{ "$group": {
"_id": "$deal_details.deals_location",
"prices": {
"$push": "$deal_details.deals_price"
}
}}
],
function(err,results) {
if (err) throw err;
console.log(JSON.stringify(results,undefined,2));
}
);
Which gives output like:
{
"_id": "101.6833,3.1333",
"prices": [
12.12,
34.3,
12.12,
34.78,
34.32
]
}
Depending on how many documents actually match the grouping.
Alternately, you might want to look at the $geoNear pipeline stage, which gives a bit more control, especially when dealing with content in arrays.
Also beware that with "location" data in an array, only the "nearest" result is being considered here and not "all" of the array content. So other items in the array may not be actually "near" the queried point. That is more of a design consideration though as any query operation you do will need to consider this.
You can merge them with reduce:
locations = locations.reduce(function(prev, location){
previous = prev.concat(location.deal_details)
return previous
},[])

Resources