Is it make sense to wrap mongoose model to own API? - node.js

I'm trying to understand is it make sense to make own API for working with Mongoose models?
Let's say we have the simple Mongoose user model:
var UserSchema = new mongoose.Schema({
email: { type: 'string', required: true, unique: true, lowercase: true },
password: { type: 'string', required: true },
name: {type: 'string'}
});
var UserModel = mongoose.model('User', UserSchema);
For a abstract application, User model should have methods like 'create', 'delete', 'update', 'find', 'authenticate' and so on. So I have two approach to achieve this purpose:
Include these methods into a Mongoose Model like the following:
UserSchema.static('create', function (data, callback) {
var user = new User(data);
user.save(function (err) {
if (err) return callback(err);
return callback(null, user);
});
});
Wrap a method in a custom User class like this:
UserProvider = function(){};
UserProvider.prototype.create = function(data, callback) {
var user = new User(data);
user.save(function (err) {
if (err) return callback(err);
return callback(null, user);
});
};
In the first one I can to create a new user like this:
UserModel.create({name: 'test'}, function (err, user) {
if (err) {// do something}
});
And in the second one I can create a new similar:
var userProvider= new UserProvider();
userProvider.create({name: 'test'}, function (err, user) {
if (err) {// do something}
});
Although these approaches look similar I feel I need choose that don't break Mongoose API in a future.
Please tell me which is approach looks better for mongoose models?

Mongoose models have native create, update, and find methods, so method one would already break Mongoose if you tried to add those.
You could use method two (which feels messy to me), but it's probably cleaner to use the existing Mongoose methods as your provider API pattern and just add the ones you need that are not provided natively via static functions. Either that or add your own full set of methods that have names that use a unique prefix (e.g. my_create, my_update, etc.).
But make sure it's clear to you why you're adding an abstraction layer, as the added indirection and complexity doesn't come free.

Related

Mongoose model methods - create document if unique

I'm new to MongoDB and Mongoose. I'm using it with my Node project (with Express), and I'm trying to keep everything organized and separated. For example, I'm trying to keep all the database queries in each model file. This way all other files could simply use User.createNew({ fields }) and a new user will be created.
I need each user to be unique (based on their usernames), and I'm not sure exactly where to keep this functionality. I set unique: true in the Schema but upon reading Mongoose's documentation, they stated that unique is not real validation (or something about how validation should happen beforehand). So my main problem is how to create a static method to create a new user, and also validate this user doesn't exist beforehand. I could implement all of this in one static method:
userSchema.statics.createUser = function (username, ..., cb) {
this.findOne({ username }, function (err) {
if (err) {
return new this({
username,
...
}).save(cb);
} else {
return Promise.reject(new Error("User already exists!"));
}
});
};
I'm pretty confused with the whole cb function and what I'm supposed to pass to it.
After reading other posts about validation, I realized I could also do something like this:
const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
validate: function (val, fn) {
this.find({ username: val }, function (err) {
fn(err || true);
});
},
message: function (props) {
`Username '${props.value}' already exists.`;
},
},
...
Also here I'm confused about what fn accepts and what it even does (I found an answer similar to this with no explanation online).
In the end, I would like to use this model in a controller to create a new user, like this
User.createNew({ username: "example", ...})
.then(doc => console.log("User was created: " + doc))
.catch(err => console.error) // The error is something custom like "This user already exists"
Any help is appreciated!

Expressjs rest api how to deal with chaining functionality

I am building a restful API using express, mongoose and mongodb. It works all fine but I have a question about how to deal with requests that contain more functionality than just one find, delete or update in the database. My user model looks as follows:
var UserSchema = new Schema({
emailaddress: {type: String, unique: true},
firstname: String,
lastname: String,
password: String,
friends: [{type: Schema.Types.ObjectId, unique: true}]
});
As you can see is the friends array just an array of ObjectIds. These ObjectIds refer to specific users in the database. If I want to retrieve an array of a user's friends I now have to look up the user that makes the request, then find all the users that have the same id as in the friends array.
Now it looks like this:
methods.get_friends = function(req, res) {
//find user.
User.findOne({_id: req.params.id}, function(err, user, next) {
if(err) next(err);
if(user) {
console.log(user);
//find friends
User.find({_id: {$in: user.friends}}, {password: 0}).exec(function (err,
friends, next) {
if(err) next(err);
if(friends) {
res.send(friends);
};
});
}
Would it be possible to seperate the lookup of the user in a certain method and chain the methods? I saw something about middleware chaining i.e. app.get('/friends', getUser, getFriend)but would that mean that I have to alter the req object in my middleware (getUser) method and then pass it on? How would you solve this issue? Would you perhaps change the mongoose model and save all friend data (means that it could become outdated) or would you create a method getUser that returns a promise on which you would collect the friend data?
I will be grateful for all the help I can get!
Thank you in advance.
Mongoose has a feature called population which exists to help in these kinds of situations. Basically, Mongoose will perform the extra query/queries that are required to load the friends documents from the database:
User.findOne({_id: req.params.id})
.populate('friends')
.exec(function(err, user) {
...
});
This will load any related friends into user.friends (as an array).
If you want to add additional constraints (in your example, password : 0), you can do that too:
User.findOne({_id: req.params.id})
.populate({
path : 'friends'
match : { password : 0 },
})
.exec(function(err, user) {
...
});
See also this documentation.

Mongoose - REST API - Schema With Query to different model

I'm trying to avoid DB Callback Queries.
Assuming that you have two schemas that looks like so :
1st) User Schema
username : {type: String, unique: true},
age : {type: Number}
2nd) Activity Schema
owner: [{type: Schema.Types.ObjectId, ref: 'User'}],
city: {type: String},
date: {type: Date}
So far so good.
Now lets say you have a route to /user/:id, what you would expect is to get the username and the age, but what if I would also like to return on that route the latest activity?
EDIT: Please note that latest activity isn't a value in the database. it's calculated automatically like activity.find({owner: ObjectId(id)}).sort({date: -1}).limit(1)
What is done right now:
User.findOne({username:req.params.username}).lean().exec(function(err,userDoc)
{
if(err) return errHandler(err);
Activity.findOne({owner:userDoc.username}).sort({date:-1}).exec(function(err,EventDoc){
if(err) return errHandler(err);
userDoc.latest_activity = EventDoc._id;
res.json(userDoc);
res.end();
})
})
The problem with the snippet above is that it is hard to maintain,
What if we want to add more to this API functionality? We would end in a callback of hell of queries unless we implement Q.
We tried to look at Virtual but the issue with that is that you can't
really query inside a mongoose Virtual, since it returns a
race-condition, and you are most likely not get that document on time.
We also tried to look at populate, but we couldn't make it since the documentation on populate is super poor.
QUESTION:
Is there anyway making this more modular?
Is there any way avoiding the DB Query Callback of Hell?
For example is this sort of thing possible?
User.findOne({username:req.params.username}).lean().populate(
{path:'Event',sort:{Date:-1}, limit(1)}
).exec(function(req,res))...
Thanks!
In this case, the best way to handle it would be to add a post save hook to your Activity schema to store the most recent _id in the latest_activity path of your User schema. That way you'd always have access to the id without having to do the extra query.
ActivitySchema.post('save', function(doc) {
UserSchema.findOne({username: doc.owner}).exec(function(err, user){
if (err)
console.log(err); //do something with the error
else if (user) {
user.latest_activity = doc._id;
user.save(function(err) {
if (err)
console.log(err); //do something with the error
});
}
});
});
Inspired by #BrianShambien's answer you could go with the post save, but instead of just storing the _id on the user you store a sub doc of only the last activity. Then when you grab that user it has the last activity right there.
User Model
username : {type: String, unique: true},
age : {type: Number},
last_activity: ActivitySchema
Then you do a post save hook on your ActivitySchema
ActivitySchema.post('save', function(doc) {
UserSchema.findOne({username: doc.owner}).exec(function(err, user){
if (err) errHandler(err);
user.last_activity = doc;
user.save(function(err) {
if (err) errHandler(err);
});
});
});
**********UPDATE************
This is to include the update to the user if they are not an owner, but a particpant of the the activity.
ActivitySchema.post('save', function(doc) {
findAndUpdateUser(doc.owner, doc);
if (doc.participants) {
for (var i in doc.participants) {
findAndUpdateUser(doc.participants[i], doc);
}
}
});
var findAndUpdateUser = function (username, doc) {
UserSchema.findOne({username: username}).exec(function (err, user) {
if (err) errHandler(err);
user.last_activity = doc;
user.save(function (err) {
if (err) errHandler(err);
});
});
});

How to check values against the DB when using the pre-save hook?

On a User schema, I'd like to check if the specified email already exists for the specified shop, before saving.
var UserSchema = new Schema({
_shop: {
type: Schema.Types.ObjectId,
ref: 'Shop',
required: true
},
email: String,
//...
});
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
// How to do use the static method isThatEmailFreeForThisShop here?
});
UserSchema.statics.isThatEmailFreeForThisShop = function(email, shop_id, cb) {
this.find({email: email, _shop: shop_id}, function(err, users) {
// ...
});
});
There could be different users with the same email as long as they are from different shops.
I do not know how to use the static method in the pre-save hook...
Thanks!
You've created a User Model instance somewhere (I'll call it User):
var User = mongoose.model('user', UserSchema);
So, the isThatEmailFreeForThisShop function is available on the User model:
User.isThatEmailFreeForThisShop(...)
From your save hook:
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
User.isThatEmailFreeForThisShop(this.email, this._shop,
function(err, result) {
if (result) { // found
// do something
return next({ error: "duplicate found" });
}
return next();
});
});
You may also want to switch to using the pre-validate rather than save.
I'd expect in your function, isThatEmailFreeForThisShop that you'd call the cb parameter when the results have been "found".
You probably would use findOne (reference) rather than find. Given that there's still a race condition, you'd want to add an index as a compound index email and shop_id and set the unique attribute to true to prevent duplicates from sneaking in (then, you'll need to handle the fact that a save on a model instance may throw an error.)
UserSchema.statics.isThatEmailFreeForThisShop = function(email, shop_id, cb) {
this.findOne({email: email, _shop: shop_id}, function(err, user) {
// ...
cb(err, user != null);
});
});

Mongoose: Sort alphabetically

I have a User model
var User = mongoose.model('Users',
mongoose.Schema({
username: 'string',
password: 'string',
rights: 'string'
})
);
I want to get all the users, sorted alphabetically by username. This is what I have tried
User.find({}, null, {sort: {username: 1}}, function (err, users) {
res.send(users);
});
However, this does not sort the users alphabetically. How can I sort alphabetically?
EDIT: I got confused because I was expecting a "purely alphabetically" sort from Mongoose, not one where Z > a. Basically, I wanted a sort based on username.toLowerCase().
This question and answer are a few years old, and from what I can tell there is now a correct way to do this. Providing this for future searchers:
User.find().collation({locale:'en',strength: 2}).sort({username:1})
.then( (users) =>{
//do your stuff
});
You could also index on username without case sensitivity:
UserSchema.index({username:1}, {collation: { locale: 'en', strength: 2}});
strength:1 is another option - best to refer to the documentation to decide which works best for you.
For the details of all this, look here.
EDIT: Per the comment the issue turns out to be sorting on toLowerCase(username). MongoDB doesn't have a built in method for complex sorting. So there are essentially two ways to go:
Add a usernameLowerCase field to the Schema. This is the better option if you need to do this a lot.
Perform an aggregation with a projection using the $toLower operator to dynamically generate a usernameLowerCase field. This comes with performance and memory caveats, but it may be the more convenient choice.
Original Answer: Here's a complete example that sorts correctly using the specific code from the question. So there must be something else going on:
#! /usr/bin/node
var mongoose = require('mongoose');
mongoose.connect('localhost', 'test');
var async = require('async');
var User = mongoose.model('Users',
mongoose.Schema({
username: 'string',
password: 'string',
rights: 'string'
})
);
var userList = [
new User({username: 'groucho', password: 'havacigar', rights: 'left'}),
new User({username: 'harpo', password: 'beepbeep', rights: 'silent'}),
new User({username: 'chico', password: 'aintnosanityclause', rights: 'all'})
];
async.forEach(userList,
function (user, SaveUserDone) {
user.save(SaveUserDone);
},
function (saveErr) {
if (saveErr) {
console.log(saveErr);
process.exit(1);
}
User.find({}, null, {sort: {username: 1}}, function (err, users) {
if (err) {
console.log(err);
process.exit(1);
}
console.log(users);
process.exit(0);
});
}
);

Resources