Mongo DB updateOne query calculation - mongodb

I have this Schema:
const HeroSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
power_start: {
type: Number,
required: true,
default: Math.floor(Math.random() * 10)
},
power_current: {
type: Number,
required: true,
default: 0
}
})
When a user clicks a button I want to randomly add 'power' between 1-10 to 'power_current'. Means, I need the final number in 'power_current' to be the sum of 'power_start' + random number + 'power_current'
'power start' getting random number as the default.
I just started to write and stuck:
updateHeroPower: async({id})=>{
const res = await HeroSchema.updateOne({
_id: id
},{
$set:{
}
})
}
Thanks

For Mongo version 4.2+ you can use pipelined updates like so:
const res = await HeroSchema.updateOne({
_id: id
},
[
{
$set: {
power_current: {$sum: ["$power_current", "$power_start", Math.floor(Math.random() * 10)]}
}
}
]
)
Older Mongo version do not have the power to access document fields within the update object.
This means you'll have to split it into 2 calls. First read the document and only then update it using the information you fetched.
One thing to note is I'm not sure you actually want the logic you requested as summing power_current and power_start every single time will actually inflate the score. I think what you want to do is sum random with power_start if power_current does not exist, and otherwise sum it with power_current as it already embodies power_start value after the initial sum.
Assuming my assumption is correct you can achieve this with $ifNull
const res = await HeroSchema.updateOne({
_id: id
},
[
{
$set: {
power_current: {$sum: [{$ifNull: ["$power_current", "$power_start"]}, Math.floor(Math.random() * 10)]}
}
}
]
)

Related

How to put First Document of a collection inside $cond for custom pagination in nestJs and mongodb

I'm Implementing Keyset pagination in a nestjs and mongodb project. And I'm trying to customize the original pagination solution. I want to make a condition if the startId document is the first document present in the collection.
This is the Code I'm trying. AND THE ISSUE WITH THIS CODE IS THAT IT RETURNS ALL THE DOCUMENTS WHATEVER THE ID YOU GIVE IN THE QUERY. I KNOW MY LOGIC COULD BE WRONG BUT ONE THING ABOUT I'M SURE AND THAT IS I'M WRITING THE SYNTAX, IS WRONG, AS THIS IS MY FIRST TIME EXPERIECE WITH MONGO QUERIES
async findAll( page?: number, documentsToSkip = 0, limitOfDocuments?: number,
startId?: string,
): Promise<userDocument[]> {
return await this.userModel
.find({
$cond: {
if: { $first: { _id: startId } },
then: { $gte: startId },
else: { $gt: startId },
},
})
.skip(documentsToSkip)
.limit(limitOfDocuments);
}
Explanation of the above code. For example I'm dealing with Users
1- If the startId(passed in query) is equal to the _id of the first document present in the user collection then the below then should be executed
then: { $gte: startId },
2- If the startId(passed in query) is not equal to the _id of the first document present in the user collection then the below else should be executed. Lets's say the pagination limit is set to 10 documents per page. And If I'm providing the id of the 11th document then else should be executed
else: { $gt: startId },
REASON FOR All THIS
The keyset solution present on the internet uses this
_id: { $gt: startId } and with this the document of the startedId is automatically skipped. SO I'M TRYING TO DO IF THE PAGINATION STARTS FROM THE FIRST DOCUMENT THEN THE FIRST DOCUMENT ITSELF SHOULD BE PRESENT AND VISIBLE. BUT THEN IF USER MOVES TO THE SECOND PAGE OF THE PAGINATION THE LAST DOCUMENT OF THE FIRST PAGE SHOULD NOT BE VISIBLE AT THE SECOND PAGE, AS THE LAST DOCUMENT ID BECOMES THE STARTING ID FOR THE SECOND PAGE DOCUMENTS
I've made the solution for my particular scenario. The issue was I hade to customize original pagination according to the first document present in the collection. But $first wasn't working for me. AS $first returns the first element of an Array and not the first document itself.
SOLUTION
1- To find the first document use findOne() method without any parameter. And it will simply return the first document of the collection.
let firstDocument: userDocument = await this.userModel.findOne();
2-For simple customization of the Pagination
if (startId == firstDocument._id) {
return await this.userModel
.find({
_id: { $gte: startId },
})
.skip(documentsToSkip)
.limit(limitOfDocuments);
} else {
return await this.userModel
.find({
_id: { $gt: startId },
})
.skip(documentsToSkip)
.limit(limitOfDocuments);
}
3- I had to make other changes as well so my original function now look different In case If someone want to know
async findAll(
page?: number,
limitOfDocuments?: number,
startId?: string,
): Promise<PaginatedUsersDto> {
let firstDocument: userDocument = await this.userModel.findOne();
let count = await this.userModel.count();
let totalPages = (count / limitOfDocuments).toFixed();
console.log('First Doucment in User Colelction', firstDocument);
if (startId == firstDocument._id) {
this.paginatedUsers = await this.userModel
.find({
_id: { $gte: startId },
})
.sort({ _id: 1 })
.skip((page - 1) * limitOfDocuments)
.limit(limitOfDocuments);
} else {
this.paginatedUsers = await this.userModel
.find({
_id: { $gt: startId },
})
.skip((page - 1) * limitOfDocuments)
.limit(limitOfDocuments);
}
let paginatedObj = {
paginatedUsers: this.paginatedUsers,
totalPages: totalPages,
count: count,
};
return paginatedObj;
}

Indexing has no effect on db.find()

I've Just started to work with MongoDB, so you might find my question really stupid.I tried to search a lot before posting my query here, Any Help would be Appreciated.
I also came across this link StackOverFlow Link, which advised to apply .sort() on every query, but that would increase the query time.
So I tried to index my collection using .createIndexes({_id:-1}), to sort data in descending order of creation time(newest to oldest), After that when I used the .find() method to get data in sorted format(newest to Oldest) I did'nt get the desired result , I still had to sort the data :( .
// connecting db
mongoose.connect(dbUrl, dbOptions);
const db = mongoose.connection;
// listener on db events
db.on('open', ()=>{console.log('DB SUCESSFULLY CONNECTED !!');});
db.on('error', console.error.bind(console, 'connection error:'));
// creating Schema for a person
const personSchma = new mongoose.Schema(
{ name: String,
age : Number}
)
// creating model from person Schema
const person = mongoose.model('person', personSchma);
// Chronological Order of Insertion Of Data
// {name: "kush", age:22}
// {name: "clutch", age:22}
// {name: "lauv", age:22}
person.createIndexes({_id:-1}, (err)=>{
if (err){
console.log(err);
}
})
person.find((err, persons)=>{
console.log(persons)
// Output
// [
// { _id: 6026eadd58a2b124d85b0f8d, name: 'kush', age: 22, __v: 0 },
// { _id: 6026facdf200f8261005f8e0, name: 'clutch', age: 22, __v: 0 },
// { _id: 6026facdf200f8261005f8e1, name: 'lauv', age: 22, __v: 0 }
// ]
})
person.find().sort({_id:-1}).lean().limit(100).then((persons)=>{
console.log(persons);
// Output
// [
// { _id: 6026facdf200f8261005f8e1, name: 'lauv', age: 22, __v: 0 },
// { _id: 6026facdf200f8261005f8e0, name: 'clutch', age: 22, __v: 0 },
// { _id: 6026eadd58a2b124d85b0f8d, name: 'kush', age: 22, __v: 0 }
// ]
})
Indexes are special data structure, which can be used to run the queries efficiently. While running the query, MongoDB tries to see which index should be used for running the query efficiently and then that index will be used.
Creating an index with {_id:-1} will create an auxiliary data structure(index) which will be sorted newest first. It doesn't affect the order of the data which we are storing.
To sort the data in descending order(newest first) we will have to explicitly add the sort operation in your query and make sure that an index for descending order _id is present.

Pulling/deleting an item from a nested array

Note: it's a Meteor project.
My schema looks like that:
{
_id: 'someid'
nlu: {
data: {
synonyms:[
{_id:'abc', value:'car', synonyms:['automobile']}
]
}
}
}
The schema is defined with simple-schema. Relevant parts:
'nlu.data.synonyms.$': Object,
'nlu.data.synonyms.$._id': {type: String, autoValue: ()=> uuidv4()},
'nlu.data.synonyms.$.value': {type:String, regEx:/.*\S.*/},
'nlu.data.synonyms.$.synonyms': {type: Array, minCount:1},
'nlu.data.synonyms.$.synonyms.$': {type:String, regEx:/.*\S.*/},
I am trying to remove {_id:'abc'}:
Projects.update({_id: 'someid'},
{$pull: {'nlu.data.synonyms' : {_id: 'abc'}}});
The query returns 1 (one doc was updated) but the item was not removed from the array. Any idea?
This is my insert query
db.test.insert({
"_id": "someid",
"nlu": {
"data": {
"synonyms": [
{
"_id": "abc"
},
{
"_id": "def"
},
10,
[ 5, { "_id": 5 } ]
]
}
}
})
And here is my update
db.test.update(
{
"_id": "someid",
"nlu.data.synonyms._id": "abc"
},
{
"$pull": {
"nlu.data.synonyms": {
"_id": "abc"
}
}
}
)
The problem broke down to the autoValue parameter on your _id property.
This is a very powerful feature to manipulate automatic values on your schema. However, it prevented from pulling as it had always returned a value, indicating that this field should be set.
In order to make it aware of the pulling, you can make it aware of an operator being present (as in cases of mongo updates).
Your autoValue would then look like:
'nlu.data.synonyms.$._id': {type: String, autoValue: function(){
if (this.operator) {
this.unset();
return;
}
return uuidv4();
}},
Edit: Note the function here being not an arrow function, otherwise it losses the context that is bound on it by SimpleSchema.
It basically only returns a new uuid4 when there is no operator present (as in insert operations). You can extend this further by the provided functionality (see the documentation) to your needs.
I just summarized my code to a reproducable example:
import uuidv4 from 'uuid/v4';
const Projects = new Mongo.Collection('PROJECTS')
const ProjectSchema ={
nlu: Object,
'nlu.data': Object,
'nlu.data.synonyms': {
type: Array,
},
'nlu.data.synonyms.$': {
type: Object,
},
'nlu.data.synonyms.$._id': {type: String, autoValue: function(){
if (this.operator) {
this.unset();
return;
}
return uuidv4();
}},
'nlu.data.synonyms.$.value': {type:String, regEx:/.*\S.*/},
'nlu.data.synonyms.$.synonyms': {type: Array, minCount:1},
'nlu.data.synonyms.$.synonyms.$': {type:String, regEx:/.*\S.*/},
};
Projects.attachSchema(ProjectSchema);
Meteor.startup(() => {
const insertId = Projects.insert({
nlu: {
data: {
synonyms:[
{value:'car', synonyms:['automobile']},
]
}
}
});
Projects.update({_id: insertId}, {$pull: {'nlu.data.synonyms' : {value: 'car'}}});
const afterUpdate = Projects.findOne(insertId);
console.log(afterUpdate, afterUpdate.nlu.data.synonyms.length); // 0
});
Optional Alternative: Normalizing Collections
However there is one additional note for optimization.
You can work around this auto-id generation issue by normalizing synonyms into an own collection, where the mongo insert provides you an id. I am not sure how unique this id will be compared to uuidv4 but i never faced id issues with that.
A setup could look like this:
const Synonyms = new Mongo.Collection('SYNONYMS');
const SynonymsSchema = {
value: {type:String, regEx:/.*\S.*/},
synonyms: {type: Array, minCount:1},
'synonyms.$': {type:String, regEx:/.*\S.*/},
};
Synonyms.attachSchema(SynonymsSchema);
const Projects = new Mongo.Collection('PROJECTS')
const ProjectSchema ={
nlu: Object,
'nlu.data': Object,
'nlu.data.synonyms': {
type: Array,
},
'nlu.data.synonyms.$': {
type: String,
},
};
Projects.attachSchema(ProjectSchema);
Meteor.startup(() => {
// just add this entry once
if (Synonyms.find().count() === 0) {
Synonyms.insert({
value: 'car',
synonyms: ['automobile']
})
}
// get the id
const carId = Synonyms.findOne()._id;
const insertId = Projects.insert({
nlu: {
data: {
synonyms:[carId] // push the _id as reference
}
}
});
// ['MG464i9PgyniuGHpn'] => reference to Synonyms document
console.log(Projects.findOne(insertId).nlu.data.synonyms);
Projects.update({_id: insertId}, {$pull: {'nlu.data.synonyms' : carId }}); // pull the reference
const afterUpdate = Projects.findOne(insertId);
console.log(afterUpdate, afterUpdate.nlu.data.synonyms.length);
});
I know this was not part of the question but I just wanted to point out that there are many benefits of normalizing complex document structures into separate collections:
no duplicate data
decouple data that is not intended to be bound (here: Synonyms could be also used independently from Projects)
update referred documents once, all Projects will point to the very actual version (since it's a reference)
finer publication/subscription handling => more control about what data flows over the wire
reduces complex auto and default value generation
changes in the referred collection's schema may have only few consequences for UI and functions that make use of the referrer's schema.
Of course this has also disadvantages:
more collections to handle
more code to write (more code = more potential errors)
more tests to write (much more time to invest)
sometimes you need to denormalize back for this one case out of 100
you have to invest a lot of time in data schema design before starting to code

How can I query the value of an object rather than the key value pair in MongoDB

I've store an array on my user object which holds all of the data
{
_id: ObjectId(#############)
fname: 'Bob',
lname: 'Vargas',
data: [
// the data I want
]
}
I am using express to get his data like this:
db.users.findOne( { _id: ObjectId(#############) }, { data: 1, _id: 0 } );
but that is giving me an object rather than the array:
{ data: [ /* my data */ ]}
how can I get just the array?
UPDATE
app.get('/user/:id/data', function (req, res, next) {
db.users.findOne(
{ _id: mongojs.ObjectId(req.params.id) },
{ data: 1, _id: 0 },
function (err, userData) {
if (err) {
res.send(err);
}
res.json(userData);
}
);
});
Add projection to query result:
db.users.findOne( { _id: ObjectId(#############) }, {_id:0, data:1} )
Use 0 to exlude field from result (_id is included by default), and 1 to include field in result.
MongoDB returns object per document. But you can manually map objects on client side:
db.users.findOne( { _id: ObjectId(#############) }, {_id:0, data:1} )
.map(function(d){ return d.data; }))
MongoDB's findOne() will only return an object, not an array; thus the One. Instead you will need to receive it as and object and then get the value.
If you are doing this from mongo shell then there is no way around unless you want to move the data into its own collection. Otherwise you can get the array from the object in your application.
UPDATE:
in your express response, only encode the data value, like this
res.json(userData.data);

Can't use $multiply with Number

I am running into an update problem using Mongoose, below is the schema definition. For example sake, below I would like to udpate the price of a car by multiplying the number of tires by 500:
car.js =
var mongoose = require('mongoose');
module.exports = mongoose.model('car', {
make : {type:String},
model : {type:String},
num_tires: {type:Number, default:0}
price : {type:Number, default:0}
});
updateCost.js =
var Car = require('car');
Car.update(
{make: 'Honda'},
{price: {$multiply: ['$num_tires', 500]}},
{multi: true},
function(err) {
console.log("Thar's an err", err);
});
The error I am receiving is: "Can't use $multiply with Number".
Is there a way around the schema definition Number to update the price? Thanks everyone for their time.
You cannot reference the current document's properties from within an update(). You'll either have to iterate through all the documents and update them or use aggregation with the $multiply expression as an arithmetic operation in aggregation within $project pipeline to multiply the num_tires field with the constant:
db.cars.aggregate([
{
$match: {
make: 'Honda'
}
},
{
$project: {
make: 1,
model: 1,
num_tires: 1,
price: {
$multiply: [ "$num_tires", 500 ]
}
}
}
])
Or you can update your schema to include an arbitrary field unit_price: {type: Number, default: 500} which you can then use as $multiply: [ "$num_tires", "$unit_price" ] in the $project pipeline.
Another alternative is to iterate through all the matched documents and update using save method like this:
var Car = require('car');
Car.find({make: 'Honda'}).snapshot().forEach(
function (e) {
// update document, using its own properties
e.price = e.num_tires * 500;
// remove old property
delete e.price;
// save the updated document
Car.save(e);
}
);
Or using the $set operator:
var Car = require('car');
Car.find().forEach(
function (elem) {
Car.update(
{
_id: elem._id,
make: "Honda"
},
{
$set: {
price: elem.num_tires * 500
}
},
{multi: true},
function(err) {
console.log("There's an error ", err);
}
);
}
);
If you had a default value for the price which is equal to the num_tires, then you may want to just update the price field without referencing another field in the same document, use the $mul operator:
var Car = require('car');
Car.update(
{make: 'Honda'},
{$mul: {price: 500}},
{multi: true},
function(err) {
console.log("There's an error ", err);
}
});