Document must have an _id before saving - node.js

I know this is an error that has been asked about several times on StackOverflow, but I can't seem to find the answer I'm looking for. I have a simple schema that stores an _id and a URL. The URL works fine, but when I go to create a new schema and save it, it states the above error even though I have prehooks to explicitly define the _id.
Here's the Schema code as well as the prehook:
const LinkSchema = new Schema({
_id: { type: Number },
url: { type: String, required: true }
}, {
timestamps: true,
collection: 'links'
});
LinkSchema.pre('save', function(next) {
// Before saving, increment the count in the linkEntryCount document in the counter collection and create the doc if not already made.
CounterModel.findByIdAndUpdate('linkEntryCount', { $inc: { count: 1 } }, { new: true, upsert: true, useFindAndModify: false }, function(err, counter) {
if(err) return next(err);
this._id = counter.count; // Create the previously undefined ObjectID with the +1'ed counter from linkEntryCount
next();
})
});
I've created an incrementing integer counter as per the MongoDB database - using a separate collection for counting. I've tested this and it works fine, and it even seems to assign the _id when the prehook is called. When I create an instance of the model and insert the URL, that's when the error appears. The document isn't even created.
Thanks for your help!

_id: { type: Number },
A mongodb _id isnt a number but a mongoose.Schema.Types.ObjectId
So replace that line with
_id: { type: mongoose.Schema.Types.ObjectId },
You should use _id only with
new mongoose.Types.ObjectId()
And then you can add like id: { type: Number } and use that as the counter

So I solved my question using this post.
The problem was that this was being used in the wrong context inside the CounterModel.findByIdAndUpdate() function. The code was trying to update a nonexistent field in the linkEntryCount collection.
Here's the fixed prehook:
LinkSchema.pre('save', function(next) {
var link = this;
// Before saving, increment the count in the linkEntryCount document in the counter collection and create the doc if not already made.
CounterModel.findByIdAndUpdate('linkEntryCount', { $inc: { count: 1 } }, { new: true, upsert: true, useFindAndModify: false }, function(err, counter) {
if(err) return next(err);
link._id = counter.count; // Create the previously undefined ObjectID with the +1'ed counter from linkEntryCount
next();
});
});
All I did was set the prehooks reference to this to a variable and then used that later in the findByIdAndUpdate() function.

Related

Mongoose- Run a function to all Entries in database

I am a beginner in NodeJs and MongoDB. I have a user schema where I have a field which is an array that is filled by the user's input value. After users enter the value, the admin also passes an array of correct answers. I want to create a function which runs on all users array field and on correct answer store the score in users schema. Just wanted to know how do I run the function on all entries of the collection.
//Final result schema by the admin
const resultSchema = new mongoose.Schema({
matchday:Number,
homeTeam:String,
awayTeam:String,
utcDate:Date,
finalUpdateTime:Date,
result:Array
})
//The predicted answer Schema
const predictSchema = new mongoose.Schema({
user:{
type:mongoose.Schema.ObjectId,
ref:'User',
required:[true, 'Predicted Team must belong to a User']
},
teamData:Array,
matchday: Number,
score:{
type:Number,
default:0
},
createdAt: {
type:Date,
default:Date.now()
},
lastUpdated:Date,
},{
toJSON: {
virtuals: true,
},
toObject: {
virtuals: true,
},
})
You can define a static method for your schema. Statics are methods that can be invoked directly by a Model.
See here
You can pass array of correct answers to this method and check the answers for each user in your collection. You can retrieve all users using Find
I managed to solve the issue and it works but not sure if its the correct way to do it
exports.updateUserScore = async (req, res, next) => {
const user = await Predict.find({ matchday: req.body.matchday });
user.map(async (el) => {
let score = 0;
el.teamData.map((e) => {
if (req.body.teamData.includes(e)) score = score + 1;
});
console.log(score, el._id);
await Predict.findByIdAndUpdate(el._id, { score: score });
});
res.status(200).json({
status: 'success',
message: 'Updated User Score Successfully',
});
};

Mongoose get collection data if present the update otherwise insert

I am new to mongodb and mongoose with ExpressJS. It may be simple question but I dont success.
var BookCounter = new Schema({
counter: {type: Number,},
book: {type: String, min: 18}
});
this is the my schema and the following is the my question:
I want to check that xyz book is present or not
if book is present
Then I have to update the book counter by one.
otherwise
I have to insert new book.
Can you please help me ..
Thanks in advance :)
You need an update operation findOneAndUpdate() that uses the options upsert to create the document if it does not exist and the new option if set to true returns the newly created/modified document rather than the original, alongside the $set and $inc field update operators.
The following example demonstrates this:
var query = { "book": "xyz" },
update = { "$inc": { "counter": 1 } },
options = { "upsert": true, "new": true };
// Find the document
Book.findOneAndUpdate(query, update, options, function(err, result) {
if (err) handleError(err);
else {
// do something with the document
console.log(JSON.stringify(result, null, 4));
}
});

Mongoose: how to check if document is modified via model.findOneAndUpdate()

In mongoose, we can check if an update operation has modified the document with model.update():
model.update(query, update, function(err, raw){
if (raw.nModified >= 1) console.log('document is modified!')
});
Is there a way to do the same with model.findOneAndUpdate()?
model.findOneAndUpdate(query, update, { new: true }, function(err, doc){
if (doc) {
// So MongoDB found the document, but is there a way
// to know the document was indeed modified?
}
});
You can pass the option { passRawResult : true } to mongoose to advice mongoose to pass the raw result of the underlying mongodb driver, in this case mongodb-native, as a third argument to the callback.
mongodb-native documentation for findOneAndUpdate
model.findOneAndUpdate(query, update, { new: true, passRawResult : true }, function(err, doc, res){
// res will look like
// { value: { _id: 56a9fc80a7f9a4d41c344852, name: 'hugo updated', __v: 0 },
// lastErrorObject: { updatedExisting: true, n: 1 },
// ok: 1 }
});
In case the update did not succeed due to no matching document was found a null res will be passed to the callback. In case a document matched but field values where the same as before the update res object will not give you enough information to figure out if values were updated for the matching document.

Mongoose findOneAndUpdate and runValidators not working

I am having issues trying to get the 'runValidators' option to work. My user schema has an email field that has required set to true but each time a new user gets added to the database (using the 'upsert' option) and the email field is empty it does not complain:
var userSchema = new mongoose.Schema({
facebookId: {type: Number, required: true},
activated: {type: Boolean, required: true, default: false},
email: {type: String, required: true}
});
findOneAndUpdate code:
model.user.user.findOneAndUpdate(
{facebookId: request.params.facebookId},
{
$setOnInsert: {
facebookId: request.params.facebookId,
email: request.payload.email,
}
},
{upsert: true,
new: true,
runValidators: true,
setDefaultsOnInsert: true
}, function (err, user) {
if (err) {
console.log(err);
return reply(boom.badRequest(authError));
}
return reply(user);
});
I have no idea what I am doing wrong, I just followed the docs: http://mongoosejs.com/docs/validation.html
In the docs is says the following:
Note that in mongoose 4.x, update validators only run on $set and $unset operations. For instance, the below update will succeed, regardless of the value of number.
I replaced the $setOnInsert with $set but had the same result.
required validators only fail when you try to explicitly $unset the key.
This makes no sense to me but it's what the docs say.
use this plugin:
mongoose-unique-validator
When using methods like findOneAndUpdate you will need to pass this configuration object:
{ runValidators: true, context: 'query' }
ie.
User.findOneAndUpdate(
{ email: 'old-email#example.com' },
{ email: 'new-email#example.com' },
{ runValidators: true, context: 'query' },
function(err) {
// ...
}
In mongoose do same thing in two step.
Find the result using findOne() method.
Add fields and save document using Model.save().
This will update your document.
I fixed the issue by adding a pre hook for findOneAndUpdate():
ExampleSchema.pre('findOneAndUpdate', function (next) {
this.options.runValidators = true
next()
})
Then when I am using findOneAndUpdate the validation is working.
I created a plugin to validate required model properties before doing update operations in mongoose.
Plugin code here
var mongoose = require('mongoose');
var _ = require('lodash');
var s = require('underscore.string');
function validateExtra(schema, options){
schema.methods.validateRequired = function(){
var deferred = Promise.defer();
var self = this;
try {
_.forEach(this.schema.paths, function (val, key) {
if (val.isRequired && _.isUndefined(self[key])) {
throw new Error(s.humanize(key) + ' is not set and is required');
}
});
deferred.resolve();
} catch (err){
deferred.reject(err);
}
return deferred.promise;
}
}
module.exports = validateExtra;
Must be called explicitly as a method from the model, so I recommend chaining it a .then chain prior to the update call.
Plugin in use here
fuelOrderModel(postVars.fuelOrder).validateRequired()
.then(function(){
return fuelOrderModel.findOneAndUpdate({_id: postVars.fuelOrder.fuelOrderId},
postVars.fuelOrder, {runValidators: true, upsert: true,
setDefaultsOnInsert: true, new: true})
.then(function(doc) {
res.json({fuelOrderId: postVars.fuelOrder.fuelOrderId});
});
}, function(err){
global.saveError(err, 'server', req.user);
res.status(500).json(err);
});
If you want to validate with findOneAndUpdate you can not get current document but you can get this keywords's contents and in this keywords's content have "op" property so solution is this :
Note : does not matter if you use context or not. Also, don't forget to send data include both "price" and "priceDiscount" in findOneAndUpdate body.
validate: {
validator: function (value) {
if (this.op === 'findOneAndUpdate') {
console.log(this.getUpdate().$set.priceDiscount);
console.log(this.getUpdate().$set.price);
return (
this.getUpdate().$set.priceDiscount < this.getUpdate().$set.price
);
}
return value < this.price;
},
message: 'Discount price ({VALUE}) should be below regular price',
}
The reason behind this behavior is that mongoose assumes you are just going to update the document, not insert one. The only possibility of having an invalid model with upsert is therefore to perform an $unset. In other words, findOneAndUpdate would be appropriate for a PATCH endpoint.
If you want to validate the model on insert, and be able to perform a update on this endpoint too (it would be a PUT endpoint) you should use replaceOne

Create unique autoincrement field with mongoose [duplicate]

This question already has answers here:
Mongoose auto increment
(15 answers)
Closed 2 years ago.
Given a Schema:
var EventSchema = new Schema({
id: {
// ...
},
name: {
type: String
},
});
I want to make id unique and autoincrement. I try to realize mongodb implementation but have problems of understanding how to do it right in mongoose.
My question is: what is the right way to implement autoincrement field in mongoose without using any plugins and so on?
const ModelIncrementSchema = new Schema({
model: { type: String, required: true, index: { unique: true } },
idx: { type: Number, default: 0 }
});
ModelIncrementSchema.statics.getNextId = async function(modelName, callback) {
let incr = await this.findOne({ model: modelName });
if (!incr) incr = await new this({ model: modelName }).save();
incr.idx++;
incr.save();
return incr.idx;
};
const PageSchema = new Schema({
id: { type: Number , default: 0},
title: { type: String },
description: { type: String }
});
PageSchema.pre('save', async function(next) {
if (this.isNew) {
const id = await ModelIncrement.getNextId('Page');
this.id = id; // Incremented
next();
} else {
next();
}
});
Yes, here's the "skinny" on that feature.
You need to have that collection in your mongo database. It acts as concurrent key allocation single record of truth if you want. Mongo's example shows you how to perform an "atomic" operation to get the next key and ensure that even there are concurrent requests you will be guaranteed to have the unique key returned without collisions.
But, mongodb doesn't implement that mechanism natively, they show you how to do it. They only provide for the _id to be used as unique document key. I hope this clarifies your approach.
To expand on the idea, go ahead and add that mongo suggested implementation to your defined Mongoose model and as you already guessed, use it in Pre-save or better yet pre-init event to ensure you always generate an id if you work with a collection server side before you save it to mongo.
You can use this.
This package every time generate unique value for this.
Package Name : uniqid
Link : https://www.npmjs.com/package/uniqid
Ignore all the above. Here is the solution
YourModelname.find().count(function(err, count){
req["body"].yourID= count + 1;
YourModelname.create(req.body, function (err, post) {
if (err) return next(err);
res.json(req.body);
});
});

Resources