How can I add helper methods to find and save objects in Mongoose. A friend told me to use helper methods but I cannot get them to work after a day. I always receive errors saying that either findOne() or save() does not exist OR that next callback is undefined (when node compiles ... before I execute it):
I've tried _schema.methods, _schema.statics... nothing works...
var email = require('email-addresses'),
mongoose = require('mongoose'),
strings = require('../../utilities/common/strings'),
uuid = require('node-uuid'),
validator = require('validator');
var _schema = new mongoose.Schema({
_id: {
type: String,
trim: true,
lowercase: true,
default: uuid.v4
},
n: { // Name
type: String,
required: true,
trim: true,
lowercase: true,
unique: true,
index: true
}
});
//_schema.index({
// d: 1,
// n: 1
//}, { unique: true });
_schema.pre('save', function (next) {
if (!this.n || strings.isNullOrWhitespace(this.n)){
self.invalidate('n', 'Domain name required but not supplied');
return next(new Error('Domain name required but not supplied'));
}
var a = email.parseOneAddress('test#' + this.n);
if (!a || !a.local || !a.domain){
self.invalidate('n', 'Name is not valid domain name');
return next(new Error('Name is not valid domain name'));
}
next();
});
_schema.statics.validateForSave = function (next) {
if (!this.n || strings.isNullOrWhitespace(this.n)){
return next(new Error('Domain name required but not supplied'));
}
var a = email.parseOneAddress('test#' + this.n);
if (!a || !a.local || !a.domain){
return next(new Error('Name is not valid domain name'));
}
next();
}
_schema.statics.findUnique = function (next) {
this.validateForSave(function(err){
if (err){ return next(err); }
mongoose.model('Domain').findOne({ n: this.n }, next);
//this.findOne({ n: this.n }, next);
});
}
_schema.statics.init = function (next) {
this.findUnique(function(err){
if (err){ return next(err); }
this.save(next);
});
}
var _model = mongoose.model('Domain', _schema);
module.exports = _model;
I believe you are running into issues because of your usage with this. Every time you enter a new function this's context is changing. You can read more about this at mdn.
Additionally your callbacks aren't allowing anything to be passed into the mongoose method. For example if I was to create the most basic "save" method I I would do the following:
_schema.statics.basicCreate = function(newDocData, next) {
new _model(newDocData).save(next);
}
Now if I wanted to search the Domain collection for a unique document I would use the following:
_schema.statics.basicSearch = function(uniqueName, next) {
var query = {n: uniqueName};
_model.findOne(query, function(err, myUniqueDoc) {
if (err) return next(err);
if (!myUniqueDoc) return next(new Error("No Domain with " + uniqueName + " found"));
next(null, myNewDoc);
});
}
Mongoose has built in validations for what you are doing:
_schema.path("n").validate(function(name) {
return name.length;
}, "Domain name is required");
_schema.path("n").validate(function(name) {
var a = email.parseOneAddress("test#" + name);
if (!a || !a.local || !a.domain) {
return false;
}
return true;
}, "Name is not a valid domain name");
It returns a boolean. If false, it passes an error to the .save() callback with the stated message. For validating uniqueness:
_schema.path("n").validate(function(name, next) {
var self = this;
this.model("Domain").findOne({n: name}, function(err, domain) {
if (err) return next(err);
if (domain) {
if (self._id === domain._id) {
return next(true);
}
return next(false);
}
return next(true);
});
}, "This domain is already taken");
You're using self = this here so that you can access the document inside the findOne() callback. false is being passed to the callback if the name exists and the result that is found isn't the document itself.
I've tried _schema.methods, _schema.statics
To clarify, .statics operate on the Model, .methods operate on the document. Zane gave a good example of statics, so here is an example of methods:
_schema.methods.isDotCom = function() {
return (/.com/).test(this.n);
}
var org = new Domain({n: "stuff.org"});
var com = new Domain({n: "things.com"});
org.isDotCom(); // false
com.isDotCom(); // true
Opinion: It's neat to have mongoose do validations, but it's very easy to forget it's happening. You also may want to have some validation in one area of your app while using different validations elsewhere. I'd avoid using most of it unless you definitively know you will have to do the same thing every time and will never NOT have to do it.
Methods/statics are a different story. It's very convenient to call isDotCom() instead of writing out a regex test every time you need to check. It performs a single and simple task that saves you some typing and makes your code more readable. Using methods for boolean checks can add a ton of readability. Defining statics like findByName (Zane's basicSearch) is great when you know you're going to do a simple query like that repeatedly.
Treat Mongoose as a utility, not as core functionality.
Related
I am trying to save the result of findOne(), however, I do not have any idea how to save this result in a variable.
function createSubscription (req, res, next) {
let product_id = "P-5JM98005MT260873LLT44E2Y" ;
let doc = null;
product.findOne({"plans.id": product_id}, { "plans.$": 1
}).sort({create_time: -1}).exec(function(err, docs) {
if(err) {
} else {
if(docs != null) {
this.doc = docs;
}
}
});
console.log(doc);
let result = null;
if (doc.create != null) {
result = processDoc(doc);
}
}
function processDoc(doc) {
//do something
return resul;
}
function processResult(result) {
//do something
}
Below, I copy the product schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ProductSchema = new Schema({
id: {
type: String,
trim: true,
required: true,
},
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
create_time: {
type: Date,
required: true,
}
});
module.exports = mongoose.model('Product', ProductSchema);
The doc is always null and does not receive the value.
In general terms, I would like to get the response product.findOne to use another function, calling by createSubscription()
To save the result of findOne() is as easy as this:
var doc = product.findOne();
The problem you're having is that you are calling processDoc() before findOne() is finished. Look into asynchronous javascript. You can fix this by using async/await like this:
async function createSubscription (req, res, next) {
var doc = await product.findOne();
processDoc(doc);
}
The reason is you are calling a callback function, which means function is asynchronous. So there is no guarantee that your query will execute and the values will be set before it reaches to,
if (doc.create != null) {
result = processDoc(doc);
}
To resolve this you may use the Async/Await Syntax:
const createSubscription = async function (params) {
try { return await User.findOne(params)
} catch(err) { console.log(err) }
}
const doc = createSubscription({"plans.id": product_id})
Since you want to query something in database, that means you already created one and saved some data in it.
However before saving data in the database you should be creating your model which should be created under models folder in Product.js(model names should start capital letter in convention) . Then you have to IMPORT it in your above page to access it. You want to query by product.id but which product's id?
Since you have req in your function, that means you are posting something. If you set up the proper settings in app.js you should be able to access to req.body which is the information that being posted by the client side.
app.js //setting for parsing(reading or accessing) req.body
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
Now we can reach req.body.id
this should be your code:
function createSubscription (req, res, next) {
const product=Product.findOne({plan.id:req.body.id})
.
.
}
If you noticed I did not put this plan.id in quotes.Because findOne() method belongs the Product model and that model belongs to the package that you are using. (In mongoose you query without quotes, in mongodb client you query in quotes)
findOne() is an asynchronous operation means result will not come to you immediately. So you should always keep it in try/catch block.
function createSubscription (req, res, next) {
try{
const product=Product.findOne({plan.id:req.body.id})
}
catch(error){console.log(error.message)} //every error object has message property
.
.
}
Lastly since you are querying only one object you do not need to sort it.
I'am newbie to nodejs and mongodb, so how can I check if an object already exist in the collections , Note that my field type in the schema is object or JSON
const BillSchema = mongoose.Schema(
{
content: {
type: Object //or JSON
},
}
);
const Bill = module.exports = mongoose.model('Bill', BillSchema);
module.exports.addBill = function (newBill, callback) {
//Check for all bill titles and content, if newBill doesn't exist then add else do nothing
Bill.count({ content: newBill.content }, function (err, count) {
//count == 0 always ???
if (err) {
return callback(err, null);
} else {
if (count > 0) {
//The bill already exists in db
console.log('Bill already added');
return callback(null, null);
} else { //The bill doesnt appear in the db
newBill.save(callback);
console.log('Bill added');
}
}
});
}
One Of Nice Question You asked, I was suppose to achieve the same task before, I make the use of mongoose-unique-validator third party npm Package, & plugin to our schema
https://www.npmjs.com/package/mongoose-unique-validator
npm install mongoose-unique-validator
var uniqueValidator = require('mongoose-unique-validator');
const BillSchema = mongoose.Schema(
{
content: {type:Object , unique:true },
}
);
BillSchema.plugin(uniqueValidator, {message: 'is already taken.'});
Usage:
module.exports.addBill = function (newBill, callback) {
newBill.save(callback);
}
I Hope If this work for you too.
I 've got an issue with mongoose schema validation, while trying to validate the schema of documents modified inside a Model::updateMany (or update + mutli:true) request.
I've got the schema below:
var BusinessesSchema = new Schema({
label: {
type: String,
required: true
},
openingDate: {
type: Date,
required: true
},
endingDate: {
type: Date,
validate: function(value) {
if (this.constructor.name === 'Query') {
// Looks like this is a validation for update request
var doc = null;
switch (this.op) {
case 'update':
case 'updateMany': {
doc = this._update.$set;
break;
}
case 'findOneAndUpdate': {
doc = this._update;
break;
}
default:
// keep null, will throw an error
}
return doc.openingDate < value;
}
else {
return this.openingDate < value;
}
}
}
});
I would like to access modified documents value ("this") inside endingDate::validate function to make sure that for each modified document ending Date is greater than beginning one .
Even, when using pre-hook (for update & updateMany, as below), I still do not find any way to access the modified documents value to perform my check when multi is set (or when calling updateMany).
BusinessesSchema.pre('update', function(next) {
this.options.runValidators = true;
this.options.context = 'query';
next();
});
BusinessesSchema.pre('updateMany', function(next) {
this.options.runValidators = true;
this.options.context = 'query';
next();
});
I probably missed something, and would really appreciate help here.
Can't do that. Best you can do with updateMany is to capture the query context, from this, and analyze the update. Something like this:
Schema.pre('updateMany', function (next) {
const update = this.getUpdate();
if (update.$set && update.$set && !validateUpdate(update.$set)) {
throw new Error('Invalid Update');
}
next();
});
If you wanna do validation on the resulting document before the update using the save hook, you can use a cursor:
Schema.pre('save', function(next) {
if (!validDoc(this)) {
throw new Error('Invalid Doc');
}
next();
}
Schema.statics.updateManyWithValidation = async function(criteria, update) {
const cursor = Model.find(criteria).cursor();
let doc = null;
do {
doc = await cursor.next();
Object.assign(doc, update);
await doc.save();
} while(doc != null);
}
Now, bear in mind this is a much more expensive operation since you're fetching the documents, applying the changes, and then saving them individually.
I have a model with a default generated value that doesn't change throughout the document lifetime except in one special case.
A document may get marked as deleted using doc.update({_id: doc._id, deleted_at: new Date()}, {overwrite: true})
In a very special case the document may be "revived" - looked up by it's id and being worked with again afterwards.
In a pre-save hook I need to perform some action (for example store a document in another collection) whenever the document is created or revived.
Consider following simplified code:
'use strict';
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
var someSchema = mongoose.Schema({
immutable: {
type: String,
default: function () {
return 'SomeVeryRandomValue';
}
}
});
someSchema.pre('save', function (next) {
if (this.isNew || this.isModified('immutable')) {
console.log('Processing pre-save hook!');
}
next();
});
var SomeModel = mongoose.model('SomeModel', someSchema, 'test');
mongoose.connection.once('open', function (err) {
var testDoc = new SomeModel({});
console.log('New: %j', testDoc.toObject());
testDoc.save(function(err) {
console.log('Initial saved: %j', testDoc.toObject());
testDoc.update({_id: testDoc._id}, {overwrite: true}, function (err) {
// at this point using mongo console:
// > db.test.findOne()
// { "_id" : ObjectId("5617b028bf84f0a93687cf67") }
SomeModel.findById(testDoc.id, function(err, reloadedDoc) {
console.log('Reloaded: %j', reloadedDoc.toObject());
console.log('reloaded isModified(\'immutable\'): %j', reloadedDoc.isModified('immutable'));
reloadedDoc.save(function(err) {
console.log('Re-saved: %j', reloadedDoc);
mongoose.connection.close();
});
});
});
});
});
And the script runtime output:
$ node mongoose-modified-test.js
New: {"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
Processing pre-save hook!
Initial saved: {"__v":0,"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
Reloaded: {"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
reloaded isModified('immutable'): false
Re-saved: {"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
The immutable is not marked as modified and IMHO it should - original document had no value for that attribute.
A work-around solution is to remove the default value for immutable attribute and define pre-validate hook like this one:
someSchema.pre('validate', function (next) {
if (this.isNew || !this.immutable) {
this.immutable = 'SomeVeryRandomValue';
}
next();
});
This is not exactly what I need because the value won't be generated until I try to validate/save the document. The pre/post-init hooks are not executed on new SomeModel({}) so I can't use those.
Should I open an issue for mongoose.js?
this.$isDefault('immutable') can be used instead.
someSchema.pre('save', function (next) {
if (this.isNew || this.$isDefault('immutable')) {
console.log('Processing pre-save hook!');
}
next();
});
Output of the script with updated pre-save hook:
$ node --harmony mongoose-modified-test.js
New: {"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Processing pre-save hook!
Initial saved: {"__v":0,"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Reloaded: {"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Processing pre-save hook!
Re-saved: {"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Thanks to #vkarpov15 for clarification.
I'm using this schema with mongoose 3.0.3 from npm:
var schema = new Schema({
_id: Schema.ObjectId,
email: {type: String, required: true, unique: true}
});
If I try to save a email that is already in db, I expect to get a ValidationError like if a required field is omitted. However this is not the case, I get a MongoError: E11000 duplicate key error index.
Which is not a validation error (happens even if I remove the unique:true).
Any idea why?
I prefer putting it in path validation mechanisms, like
UserSchema.path('email').validate(function(value, done) {
this.model('User').count({ email: value }, function(err, count) {
if (err) {
return done(err);
}
// If `count` is greater than zero, "invalidate"
done(!count);
});
}, 'Email already exists');
Then it'll just get wrapped into ValidationError and will return as first argument when you call validate or save .
I had some issues with the approved answer. Namely:
this.model('User') didn't work for me.
the callback done wasn't working properly.
I resolved those issues by:
UserSchema.path('email').validate(async (value) => {
const emailCount = await mongoose.models.User.countDocuments({email: value });
return !emailCount;
}, 'Email already exists');
I use async/await which is a personal preference because it is much neater: https://javascript.info/async-await.
Let me know if I got something wrong.
This is expected behavior
The unique: true is equivalent to setting an index in mongodb like this:
db.myCollection.ensureIndex( { "email": 1 }, { unique: true } )
To do this type of validation using Mongoose (Mongoose calls this complex validation- ie- you are not just asserting the value is a number for example), you will need to wire in to the pre-save event:
mySchema.pre("save",function(next, done) {
var self = this;
mongoose.models["User"].findOne({email : self.email},function(err, results) {
if(err) {
done(err);
} else if(results) { //there was a result found, so the email address exists
self.invalidate("email","email must be unique");
done(new Error("email must be unique"));
} else {
done();
}
});
next();
});
Simply response to json
try {
let end_study_year = new EndStudyYear(req.body);
await end_study_year.save();
res.json({
status: true,
message: 'បានរក្សាទុក!'
})
}catch (e) {
res.json({
status: false,
message: e.message.toString().includes('duplicate') ? 'ទិន្នន័យមានរួចហើយ' : e.message.split(':')[0] // check if duplicate message exist
})
}
Sorry for answering an old question. After testing I feel good to have find these answers, so I will give my experience. Both top answers are great and right, just remember that:
if your document is new, you can just validate if count is higher than 0, thats the common situation;
if your document is NOT new and has modified the unique field, you need to validate with 0 too;
if your document is NOT new and has NOT being modified, just go ahead;
Here is what I made in my code:
UserSchema.path('email').validate(async function validateDuplicatedEmail(value) {
if (!this.isNew && !this.isModified('email')) return true;
try {
const User = mongoose.model("User");
const count = await User.countDocuments({ email: value });
if (count > 0) return false;
return true;
}
catch (error) {
return false;
}
}, "Email already exists");