Mongoose: $inc not working - node.js

I am not sure what the problem is, as I've read numerous examples.
Taken from what I was advised here in this StackOverFlow(Mongoose - Increment a value inside an array of objects), I changed the format of poll at ease to accommodate what was recommended.
So I was able to create a document format as so:
{
"_id": "584c4160b3b22e1bdad59bce",
"title": "Food",
"description": "test",
"labelOptions": {
"burger": 29,
"coffee": 44,
"pizza": 23
},
"date": "Dec 10, 2016",
"__v": 0
}
Here's what I have so far:
Poll Model
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const pollData = new Schema({
title: String,
description: String,
labelOptions: {},
date: String
})
module.exports = mongoose.model('PollData', pollData)
Using express and mongoose, here's what I have:
app.put('/polls/:id', function(req, res){
let id = req.params.id;
let labelOption = req.query.labelOption;
let query = `labelOptions.${labelOption}`
Poll.findByIdAndUpdate(
id,
{$inc: { query: 1 } },
function(err, document){
console.log(err)
console.log(document)
}
)
})
In my terminal, I see that console.log(document it receives the document I was looking for but it does not update the value at all.
Am I setting up the Model correctly? Or does Mongoose does not support template strings?
***update
This is snippet of how I am creating documents
let labelOptions = {}; <=== creating object literal to hold as placeholder
const title = req.body.title;
const description = req.body.description;
req.body.labelOptions.split(/,\s*/).map( prop =>{
labelOptions[prop] = 0 // set counter to default 0
})
const poll = new Poll({
title: title,
description: description,
labelOptions: labelOptions,
date: moment().format('MMM D, YYYY')
});
poll.save(function(err) {
if (err) { return next(err); }
res.json({ message : 'Poll added!'})
});

After doing some research across the internet, I found the reason why it wasnt working: You can't initialize objects with 'dynamic' keys.
Source: Mongoose update on a string variable not working?
By knowing that, it was just a simple solution to initialize an literal object as so:
let id = req.params.id;
let labelOption = req.query.labelOption;
let query = "labelOptions." + labelOption
let obj = {
[query] : 1
}
Poll.findByIdAndUpdate(
id,
{$inc: obj },
function(err, document){
console.log(err)
console.log(document)
}
)

Related

Saving an array to MongoDB database

Let's say I have a schema for my MongoDb collection.
module.exports = (mongoose, Schema) => {
let eventSchema = new Schema({
name: String
values: ???
});
return mongoose.model('Event', eventSchema);
};
I receive a json like this one:
{"1965": 10000000, "1966": 20000000, "1967": 300000}
I want this array to be in the "values" field of the eventSchema. How do I save it to my db?
Years vary, and the number of objects in array is always different.
Try something like this:
const payload = { name : "test" , values : {"1965": 10000000, "1966": 20000000, "1967": 300000}
}
const event = new Event(payload)
event.save(function (err) {
if (err) return console.log(err);
// saved!
});
and make sure your schema is :
let eventSchema = new Schema({
name: String
values: Object
});

mongoose filter by multiple conditions and execute to update data

I am wondering what would be the best approach to make schema functions using mongoose. I have never used this so the way I think is somewhat limited, same goes for looking for docs, without knowing what's available, is not very efficient.
Through docs I found that either using findOneAndUpdate might solve the problem; but there are some constraints.
Here is the code I am planning to run:
models/Bookmark.js
const mongoose = require('mongoose')
const bookmarkItemSchema = new mongoose.Schema({
restaurantId: String,
cachedAttr: {
name: String,
latitude: Number,
longitude: Number,
},
})
const bookmarkListSchema = new mongoose.Schema({
listName: String,
items: [bookmarkItemSchema],
})
const bookmarkSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
lists: [bookmarkListSchema],
})
// const add = (lists, userId) => {
// let bookmark = Bookmark.findOne({userId})
// bookmark.lists.listName === lists.listName //current, new
// ? bookmark.lists.items.push(lists.items)
// : bookmark.lists.push(lists)
// return bookmark
// }
mongoose.model('Bookmark', bookmarkSchema)
Routes/bookmark.js
router.post('/bookmarks', async (req, res) => {
const {lists} = req.body
console.log(lists)
if (!lists) {
return res.status(422).send({error: 'You must provide lists'})
}
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
try {
// const bookmark = Bookmark.add(lists, req.user._id, obj)
// await bookmark.save()
// res.send(bookmark)
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){ // THIS IS UNDEFINED. How to get this object?
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
} catch (e) {
res.status(422).send({error: e.message})
}
})
The req.body looks like this:
{
"lists": {
"listName": "My Saved List",
"items": {
"restaurantId": "abcdefg",
"cachedAttr": {
"name": "abcdefg",
"latitude": 200,
"longitude": 200
}
}
}
}
Basically what I commented out in the models/Bookmark.js file is what I would really like to do.
If the userId's list name already exists, then I would like to just add an item to the list.
Otherwise, I would like to add a new list to the object.
What is the best approach for doing this? Is there a straight forward mongoose api that I could use for this problem? or do I need to make two separated function that would handle each case and make that as schema methods and handle it in the routes file?

How do I keep mongo's array index from changing during an update?

I am trying to update an array within my object. However, every time I send the post call, the index in the array changes.
I have tried using $set and manually updating the array... but the index on the array keeps changing.
Here is the model:
const MappedImageSchema = new Schema({
imageUrl: {type: String, required: true},
name: {type: String, required: true},
areas:[
{
name: {type: String},
shape: {type: String},
coords:[{type: Number}],
}
]
});
module.exports = MappedImage = mongoose.model('mappedImages', MappedImageSchema)
Here is the code that performs the update:
// #route POST api/maps/:id/areas
// #desc add an area to a map (by map id)
// #access Private
router.post('/:id/areas/:id_area', passport.authenticate('jwt', { session: false }),
(req, res) => {
MappedImage.findById(req.params.id)
.then(map => {
// get all of the areas from the map...
var allAreas = map.areas;
// now get the index of the area we are going to update
const areaIndex = map.areas.map(item => item._id.toString()).indexOf(req.params.id_area);
// update the information
var coords = req.body.coords.split(',');
const updatedArea = {
name: req.body.name,
shape: req.body.shape,
coords: coords,
};
// set the updated information in the correct map area
allAreas[areaIndex] = updatedArea;
var query = {_id: req.params.id}; // this is the MAP id
var update = {$set: {areas:allAreas}}; // update the areas
var options = {new: true};
MappedImage.findOneAndUpdate(query, update, options)
.then(map => res.json(map))
.catch(err => res.status(404).json({ mapnotfound: err }));
})
.catch(err => res.status(404).json({ mapnotfound: 'Map not found while updating area' }));
}
);
Here is the data BEFORE the call
{
"_id": "5c5c69dda40e872258b4531d",
"imageUrl": "url test",
"name": "name test",
"areas": [
{
"coords": [1,2,3,4,5,6],
"_id": "5c5c8db2f904932dd8d4c560", <---- _id changes every time !
"name": "area name",
"shape": "poly"
}
],
"__v": 3
}
Here is the Postman call I make:
The result of the call is the name gets changed... but so does the index... making the next call fail with "no area found with that index".
What is perplexing about this problem is the _id for the map does not get updated when I run this code:
router.post('/:id', passport.authenticate('jwt', { session: false }),
(req, res) => {
var query = {_id: req.params.id};
var update = {imageUrl: req.body.imageUrl, name: req.body.name};
var options = {new: true};
MappedImage.findOneAndUpdate(query, update, options)
.then(map => res.json(map))
.catch(err => res.status(404).json({ mapnotfound: err }));
});
Update 1
I tried using the areas index and updating just that area... but the _id changes with this code as well:
... same code all the way down to here
allAreas[areaIndex] = updatedArea;
// but instead of calling 'findOneAndUpdate'... call map save
map.save().then(map => res.json(map));
Update 2
I can't get this code to work as areas._id and areas.$ are undefined ?
var query = {_id: req.params.id, areas._id: id_area}; // this is the MAP id
var update = {$set: {areas.$: updatedArea}}; // update the area
Update 3
So, putting the _id in the updatedArea fixes this issue... but it "feels" wrong to do so: ( per eol answer )
const updatedArea = {
_id: req.params.id_area,
name: req.body.name,
shape: req.body.shape,
coords: coords,
};
Update 4
eol - thanks for the verification on the mongoDB side... If that solves the DB id problem... I just need to know why my query is failing. I tried this and all I see in the terminal output is "creating query"... I never see the "query" and it's definition... so something is wrong and I don't know how to figure out what. Here is what I have now:
console.log('creating query');
var query = {"_id": req.params.id, "areas._id": id_area};
console.log('query');
console.log(query);
Update 5
Figured it out why the query not being output, id_area is not defined... but req.params.id_area is !
console.log('creating query');
var query = {"_id": req.params.id, "areas._id": req.params.id_area};
console.log('query');
Update 6
Code is in... but it is still not working. A picture is worth a 1000 words... so here are two:
This one shows the areas ID is still changing:
Here is the code I have now:
console.log('Update area');
console.log('changing area ' + req.params.id_area);
//console.log(req.body);
const { errors, isValid } = mapValidators.validateAreaInput(req.body);
// Check Validation
if(!isValid){
return res.status(400).json(errors);
}
MappedImage.findById(req.params.id)
.then(map => {
// Check to see if area exists
if (
map.areas.filter(
area => area._id.toString() === req.params.id_area
).length === 0
) {
return res.status(404).json({ areanotfound: 'Area does not exist' });
}
console.log('area exists');
// get all of the areas from the map...
var allAreas = map.areas;
console.log('all areas');
console.log(allAreas);
// now get the index of the area we are going to update
const areaIndex = map.areas.map(item => item._id.toString()).indexOf(req.params.id_area);
console.log('area index');
console.log(areaIndex);
// update the information
var coords = req.body.coords.split(',');
const updatedArea = {
name: req.body.name,
shape: req.body.shape,
preFillColor: req.body.preFillColor,
fillColor: req.body.fillColor,
coords: coords,
};
console.log('updated area');
console.log(updatedArea);
// set the updated information in the maps areas
allAreas[areaIndex] = updatedArea;
console.log('creating query');
var query = {"_id": req.params.id, "areas._id": req.params.id_area};
console.log('query');
console.log(query);
var update = {$set: {"areas.$": updatedArea}};
console.log('update');
console.log(update);
var options = {new: true};
MappedImage.findOneAndUpdate(query, update, options)
.then(map => res.json(map))
.catch(err => res.status(404).json({ mapnotfound: err }));
})
.catch(err => res.status(404).json({ mapnotfound: 'Map not found while updating area' }));
Here is the terminal output:
You could try setting the _id property in the updatedArea object with the value of the area that you'd like to update. This would prevent creating a new id while using the $set operator. Something like this:
// now get the index of the area we are going to update
const areaIndex = map.areas.map(item => item._id.toString()).indexOf(req.params.id_area);
// update the information
var coords = req.body.coords.split(',');
const updatedArea = {
_id: id_area,
name: req.body.name,
shape: req.body.shape,
coords: coords,
};
...
Note that with the above solution you're always setting a new array, which is why new id's are generated.
You could also try updating the specific element in the array using the $ operator:
var query = {"_id": req.params.id, "areas._id": id_area}; // this is the MAP id
var update = {$set: {"areas.$": updatedArea}}; // update the area
See the screenshots below for an example (executing the commands in the mongodb-shell) where I'm trying to only update the second array element (i.e. with _id 5c5c8db2f904932dd8d4c561)

Get the _id of the sub-document from mongoose findOne query

The schema of my Sample model is:-
var nameSchema = new mongoose.Schema({
firstname:String,
lastname:String
})
var sampleSchema= new mongoose.Schema({
number: {
type: String
},
name :{
type : [nameSchema]
}
});
I am trying to update the first and last name by searching them by their number property by making use of Sample.findOne({number:number}). And i am performing the update operation in the following manner:-
module.exports.updateNumber = function(req, res){
var number= req.body.number;
var lname= req.body.lname;
var fname= req.body.fname;
Sample
.findOne({number:number})
.select('name')
.exec(function(err, doc){
console.log(doc)
var this_id;
var thisService = doc.name.id('this_id');
thisService.firstname=fname;
thisService.lastname=lname;
doc.save(function(err, update) {
if (err) {
res
.status(500)
.json(err);
} else {
res
res.render('done')
}
});
})
}
If i console log the output i got is:
{ _id: 5bc5d71f47ff14361c0639d1,
name:
[ { _id: 5bc5d71f47ff14361c0639d2,
firstname: 'firstname',
lastname: 'lastname' } ] }
Is there any way, i could store _id: 5bc5d71f47ff14361c0639d2 in 'this_id' variable, so that this updation would be possible
name is an array, so if you want the first _id then name[0]._id would suffice, if you want an array of all values for _id in name, then name.map((person) => person._id) would give you an array of _id
However, more details about the context of this object would help give a better answer.

MongoDB and Nodejs insert ID with auto increment

I am new to NodeJs and MongoDB, i want to insert row with auto increment primary key 'id'. also defined a function called getNextSequence on mongo server.
this is working perfect on Mongodb server
> db.user.insert({
"id" : getNextSequence('user_id'),
"username" : "test",
"email" : "test#test.com",
"password" : "test123"
})
now i want to insert from NodeJs.I have tried this but not working
db.collection('user').insertOne({
id : "getNextSequence('user_id')",
username : query.name,
email: query.email,
password: query.pass
}, function(err, result) {
assert.equal(err, null);
console.log("row insterted ");
callback();
});
Assuming that getNextSequence is a server-script function (i.e. a method you defined and saved via db.system.js.save), it is not callable outside of the server. One way to go is to use eval, which forces the server to evaluate a string as a js code, even though it is not a good practice. Here is an example:
db.eval('getNextSequence(\'user_id\')', function(err, result) {
db.collection('users').insert({
"id" : result,
"username" : "test",
"email" : "test#test.com",
"password" : "test123"
});
});
Another way is to follow the mongo tutorial and to implement the getNextSequence directly in NodeJS. The syntax is pretty much the same:
function getNextSequence(db, name, callback) {
db.collection("counters").findAndModify( { _id: name }, null, { $inc: { seq: 1 } }, function(err, result){
if(err) callback(err, result);
callback(err, result.value.seq);
} );
}
You then use it in your nodeJS code like:
getNextSequence(db, "user_id", function(err, result){
if(!err){
db.collection('users').insert({
"_id": result,
// ...
});
}
});
Note: of course, you need to have set the counters collection as explained in the docs.
You can also use "mongoose-auto-increment".
The code has just 4 lines
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
autoIncrement.initialize(mongoose.connection);
userSchema.plugin(autoIncrement.plugin, 'user');
example :
npm i mongoose-auto-increment
connections.js :
const mongoose = require('mongoose');
require("dotenv").config;
const uri = process.env.MONGOURL;
mongoose.connect(uri, { useNewUrlParser: true }, (err) => {
if (!err) { console.log('MongoDB Connection Succeeded.') }
else { console.log('Error in DB connection : ' + err) }
});
require('../schema/userSchema');
userSchema.js :
var mongoose = require('mongoose'); // 1. require mongoose
var autoIncrement = require('mongoose-auto-increment'); // 2. require mongoose-auto-increment
var userSchema = new mongoose.Schema({
name: { type: String },
password: { type: String },
email: { type: String, unique: true, required: 'This field is required.' },
});
autoIncrement.initialize(mongoose.connection); // 3. initialize autoIncrement
userSchema.plugin(autoIncrement.plugin, 'user'); // 4. use autoIncrement
mongoose.model('user', userSchema);
To accomplish this, we will create a function that will keep trying to save the document untill it will have been saved with incremented _id
async function retryUntilSave(db, task) {
try {
const index = await db.collection('tasks').find().count() + 1;
const result = await db.collection('tasks').insertOne(Object.assign(task, { _id: index }))
} catch (error) {
if (error.message.includes("_id_ dup key")) {
console.log("ID already exists!")
console.log("Retrying...");
retryUntilSave(db, task)
} else {
console.log(error.message);
}
}
}
We can use task._id: index instead of Object.assign()
finally you can test this by making some concurrent requests
for (let index = 0; index < 20; index++) {
setTimeout(async () => {
await retryUntilSave(db, { title: "Some Task" })
}, 1000);
}
This function will handle easily if two or more tasks submitted at the same time because mogod throws error when we try to insert a document with duplicate _id, then we will retry saving the document again with incremented _id and this process will run until we save the document successfully !
You can also use "mongodb-autoincrement" module of node js. For example:
var autoIncrement = require("mongodb-autoincrement");
exports.yourMethod = function(newData, callback) {
autoIncrement.getNextSequence(db, your-collection-name, function (err, autoIndex) {
newData.id = autoIndex;
//save your code with this autogenerated id
});
}
You can use the below package on a model schema to auto-increment your collection field.
mongoose-auto-increment //you can download it from npm
Here I am not focusing on how to connect MongoDB. I just focus on how you can integrate auto increment in your model/collection/table.
const mongoose = require("mongoose"); //
const autoIncrement = require("mongoose-auto-increment");
const post_schema = new mongoose.Schema({
title: {
type: String,
required: true,
min: 3,
max: 225,
},
slug: {
type: String,
required: true,
},
});
autoIncrement.initialize(mongoose.connection);
post_schema.plugin(autoIncrement.plugin, {
model: "post", // collection or table name in which you want to apply auto increment
field: "_id", // field of model which you want to auto increment
startAt: 1, // start your auto increment value from 1
incrementBy: 1, // incremented by 1
});
module.exports = mongoose.model("post", post_schema);

Resources