Mongoose: Merging two documents that reference each other - javascript

I am currently trying to learn how to work with NoSQL, coming from a relational database background. In this project, I am using Express with Mongoose.
I am struggling with callbacks as I try to merge two models together, which reference each other. I am trying to edit each item in a group of one model (Ribbits) to contain the attributes of another (Users who posted a Ribbit). Because the call to find the User associated with a Ribbit is asynchronous, I am unable to return the collection of edited Ribbits (with user info).
In my website, I have ribbits (a.k.a. tweets) which belong to users. Users can have many ribbits. In one of my pages, I would like to list all of the ribbits on the service, and some information associated with the user who posted that ribbit.
One solution I found was embedded documents, but I discovered that this is, in my case, limited to showing ribbits which belong to a user. In my case, I want to start by getting all of the ribbits first, and then, for each ribbit, attach info about who posted that.
Ideally, I'd want my schema function to return an array of Ribbit objects, so that I can then render this in my view.
// models/user.js
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var userSchema = Schema({
username: String,
email: String,
password: String,
name: String,
profile: String,
ribbits: [{
type: Schema.Types.ObjectId,
ref: 'Ribbit',
}]
});
module.exports = mongoose.model('User', userSchema);
// models/ribbit.js
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
User = require('./user');
var ribbitSchema = Schema({
content: { type: String, maxlength: 140 },
created: { type: Date, default: Date.now() },
owner: { type: Schema.Types.ObjectId, ref: 'User' },
});
ribbitSchema.methods.getOwnerObj = function(cb) {
return User.findOne({ _id: this.owner }, cb);
}
ribbitSchema.statics.getAllRibbits = function(cb) {
this.find({}, function(err, ribbits) {
console.log('Before Transform');
console.log(ribbits);
ribbits.forEach(function(ribbit) {
ribbit.getOwnerObj(function(err, owner) {
ribbit = {
content: ribbit.content,
created: ribbit.created,
owner: {
username: owner.username,
email: owner.email,
name: owner.name,
profile: owner.profile,
}
};
});
});
});
}
module.exports = mongoose.model('Ribbit', ribbitSchema);

If I understand correctly, you can use Mongoose populate method for this scenario:
ribbitSchema.statics.getAllRibbits = function(cb) {
this.find({}).populate('owner').exec(function(err, ribbits){
console.log(ribbits[0].owner)
return cb(err, ribbits);
})
}

Related

How to create a new object in a collection that have a populate field

I'm currently on a school project. I have to create a web site and I wanted to do a MEAN app. All was fine until I wanted to design good models for mongoose. I will have Users (name, psswd, etc ) in one collection and I will have their "detailed information" in another collection.
I know how to query, find and assemble the data. It's all in the mongoose documentation but...
I don't know how to create (when answering a POST request) the new user with it's dedicated "detailed information".
What is the populate method for :
~User.cardid.create()~ ?
~
var cardIDSchema = new mongoose.Schema({
userID: String,
name: String,
surname: String,
date_Of_Birth: Date,
address: String,
var userSchema = new mongoose.Schema({
email: String,
psswd: String,
cardID: { type: mongoose.Schema.Types.ObjectId, ref: 'CardID'}
});~
If you're coming from the SQL world, the populate method works like the join statement where it uses the reference column in one table to fetch the corresponding items for external tables. In your schema above, if you were to fetch a use and wish to retrieve the accompanying detailed information, you use the populate method like so User.findOne().populate('cardId');
I've modified the schema below to make it easier.
To create a new user with corresponding detailed information with this schema:
var cardIDSchema = new mongoose.Schema({
name: String,
surname: String,
date_Of_Birth: Date,
address: String,
})
var Card = mongoose.model('Card', cardIDSchema);
var userSchema = new mongoose.Schema({
email: String,
psswd: String,
cardID: { type: mongoose.Schema.Types.ObjectId, ref: 'CardID'}
});
const User = mongoose.model('user', userSchema);
you have to create the card information first to get the ObjectId then the user follows.
//...
// Create the card first.
var card = new Card({
name: 'Some',
surname: 'Person',
date_Of_Birth: Date.now(),
address: 'Somewhere in the world'
}); // this will create a new card._id automatically
// Create the user and pass the card._id as the reference.
var user = new User({
email: 'test#example.org',
psswd: 'somesecret',
cardID: card._id
});
/**
* Depending on if you're using an async/await function...
*/
// For async...
// Save the card information first.
await card.save();
// Then save the user information.
await user.save();
// For callbacks...
card.save(function (err) {
if (err) ; // Do something with error.
user.save(function (err) {
if (err) ; // Do something with the error.
});
//...
To retrieve the model is even easier, which is why the populate method was created.
//...
// Say the id of the user you created was '5cd47029324acd59fc666df6'
// Using the async/await function...
var user = await User.findById('5cd47029324acd59fc666df6').populate('cardID');
// Using callback...
User.findById('5cd47029324acd59fc666df6')
.populate('cardID')
.exec(function (err, user) {
if (err) ; // Do something with error.
// Continue with user here...
});
// The user variable will then be...
/**
* {
* _id: '5cd47029324acd59fc666df6',
* email: 'test#example.org',
* psswd: 'somesecret',
* cardID: {
* _id: '5cd47652324acd59fc666df7',
* name: 'Some',
* surname: 'Person',
* date_Of_Birth: Date.now(),
* address: 'Somewhere in the world'
* }
* }
*/
//...
P.S.: To ensure consistency, use the fawn npm module which imitates a sort of transactional state to ensure that all data is saved before proceeding and if one of the models isn't save, the initially saved model will be deleted from the database.

Testing validation in Mongoose without hitting the database

If I have in my application the following model:
// employee.js
const mongoose = require('mongoose');
const validators = require('./validators');
const EmployeeSchema = new mongoose.Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
referredBy: {
type: Schema.Types.ObjectId,
ref: 'Employee',
validate: {
isAsync: true,
validator: validators.referralValidator
}
}
});
module.exports = mongoose.model('Employee', EmployeeSchema);
Basically, it's a model for an employee in a firm. That employee could have been referred to work at the firm by a different, existing, employee, so there is a self-referencing in the Employees collection.
The validation makes sure that the user doesn't enter an id of an employee who doesn't exist in the database already. It looks like this:
// validators.js
const mongoose = require('mongoose');
function referralValidator(val, callback) {
if (val) {
const Employee = mongoose.model('Employee');
Employee.findById(val.toString(), (err, found) => {
callback(found !== null);
});
} else {
callback(true);
}
}
module.exports = {
referralValidator
};
Now, I would like to test that this validation work (could be one of many other validations that I'll write in the future for other fields that will be added to the model). However, I would like not to hit the database in the test, so I want to stab out the findById to control manually what the value of "found" will be for the test. I couldn't figure out how to go around the mongo driver that mongoose employs behind the scenes.
How could I stab this function? Or, alternatively, is there a better way to implement the validator in order to make it more testable?
This link should be a good starting point on how to test model validations. If you want to mock database, then check out mockgoose

Removing element from nested array in Mongoose

I'm working on an upvoting/downvoting application using MongoDB and Node.JS
I have created two interlinked schemas:
var mongoose = require('mongoose');
var Voters = require('./voters');
var PostSchema = new mongoose.Schema({
title: String,
link: String,
upvotes: {type: Number, default: 0},
voters: [Voters.schema],
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }]
});
mongoose.model('Post', PostSchema);
and for voters:
var mongoose = require('mongoose');
var votersSchema = new mongoose.Schema({
voter_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
votetype: Number
});
module.exports = mongoose.model('Voters', votersSchema);
For including users in the voters array, I'm using this code:
var voterModel = new Voters();
voterModel.voter_id = req.payload._id;
voterModel.votetype = 1;
foundPost.voters.push(voterModel);
foundPost.save();
Which works just fine. For removing users I tried several methods, but none seem to work. The current one is $pull:
foundPost.update({'voters.voter_id': req.payload._id}, {$pull: {'voters': {'voter_id': req.payload._id, 'votetype': 1}}}, function(err){
if (err) { console.log(err); }
});
The update action works in the mongo shell, but not from within node. I also tried foundPost.voters.remove, but the result was the same. Also tried Voters.findOne, but the query always returns null.
Any help would be appreciated.
Use the id method first to find the voter then remove it and last save document to apply changes:
var voter = foundPost.voters.id(req.payload._id).remove();
foundPost.save(function (err) {
if (err) return handleError(err);
console.log('the voter was removed')
});

Can you search other models with instance methods in Mongoose?

When do models receive their prototype?
I know that embedding is generally the answer here, but I have a special case.
If I call to another model in an instance's custom method, it seems to fail.
The error I'm getting:
Fish.find is not a function at model.UserSchema.methods.fishes
The Fish model is made into a model:
// Require mongoose to create a model.
var mongoose = require('mongoose'),
User = require('./user.js');
// Create a schema of your model
var fishSchema = new mongoose.Schema({
name: String,
category: String,
user: { type: mongoose.Schema.Types.ObjectId, ref:'User' }
});
// Create the model using your schema.
var Fish = mongoose.model('Fish', fishSchema);
// Export the model of the Fish.
module.exports = Fish;
The User model calls to the Fish model within the fishes custom instance method:
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
bcrypt = require('bcrypt-nodejs'),
Fish = require('./fish');
//||||||||||||||||||||||||||--
// CREATE USER SCHEMA
//||||||||||||||||||||||||||--
var UserSchema = new Schema({
name: { type: String, required: true },
phoneNumber: {
type: String,
required: true,
index: { unique: true },
minlength: 7,
maxlength: 10
},
password: { type: String, required: true, select: false }
});
// … some bcrypt stuff…
// Access user's fishes - THIS IS WHAT'S MESSING UP!!
UserSchema.methods.fishes = function(callback) {
Fish.find({user: this._id}, function(err, fishes) {
callback(err, fishes);
});
};
module.exports = mongoose.model('User', UserSchema);
When I call .fishes() in my seeds, it claims that Fish.find is not a function.
Why!? Any help would be greatly appreciated!
The issue is a circular import (fish.js requires user.js that requires fish.js, etc).
You can work around that by resolving the model class at runtime:
UserSchema.methods.fishes = function(callback) {
mongoose.model('Fish').find({user: this._id}, function(err, fishes) {
callback(err, fishes);
});
};

Manage authentication of different types of users in express.js using passport.js

I am having problem managing the states of different types of users using Passport.js in Express.js 4.x.
I have 3 kinds user collections in my mongodb database
1. Member (has his own profile page)
2. Operator (has his own dashboard)
3. Admin (handles the backend)
I have created their separate Login/Registration systems. But only member seems to work, and the others don't. I have even written different sets of login/registration strategies for each user.
Like for the member passport.use('signup') and passport.use('login').
for operator passport.use('op-signup') and passport.use('op-login') and so on.
What I think is that I am not using the correct approach for handling users, means the collections don't need to be separated but role based in a single collection. Right ?
Here is the current mongoose models I have right now;
// Member Schema
var MemberSchema = new Schema({
username: String,
password: String,
name: { first: String, last: String },
locality: String,
// and other attributes
});
module.exports = mongoose.model('Member', MemberSchema);
// OperatorSchema
var OperatorSchema = new Schema({
username: String,
password: String,
name: { first: String, last: String },
officeAddress: String,
privatePhone: Number,
// and other attributes related to the operator
});
module.exports = mongoose.model('Operator', OperatorSchema);
Is the above approach correct or like this ?
var UserSchema = new Schema({
username: String,
password: String,
roles: {
member: { type: Schema.Types.ObjectId, ref: 'Member' },
operator: { type: Schema.Types.ObjectId, ref: 'Operator' },
admin: { type: Schema.Types.ObjectId, ref: 'Admin' }
}
});
module.exports = mongoose.model('User', UserSchema);
// and then plug the sub models to this parent one
// Member Schema
var MemberSchema = new Schema({
_user: { type: Schema.Types.ObjectId, ref: 'User' },
name: { first: String, last: String },
locality: String,
// and other attributes
});
module.exports = mongoose.model('Member', MemberSchema);
// OperatorSchema
var OperatorSchema = new Schema({
_user: { type: Schema.Types.ObjectId, ref: 'User' },
name: { first: String, last: String },
officeAddress: String,
privatePhone: Number,
// and other attributes related to the operator
});
module.exports = mongoose.model('Operator', OperatorSchema);
I am quite confused here and in a stuck situation, because when a user state is managed in session after login, the user object is exposed to the request object, and so it can only handle one type of user at a time, and may be member, operator and admin can't log in at the same time from the same browser.
So how do I manage all of these user as different instances in the browser ?
I am quite a newbie in Node.js and coming from a PHP background where managing user states was a breeze :)
What i would do is to add plugins, because you are duplicating username and password field, it is very redundant
models/plugins/member.js
module.exports = function(schema) {
schema.add({
// All the appropriate fields that your member schema need
role: String,
});
}
models/user.js
var member = require(./plugins/member);
var UserSchema = mongoose.Schema({
username: String,
password: String
});
UserSchema.plugins(member);
Later on when you want to check which user could access to which route, use middleware to check it
create this in your passport configuration
exports.requireRole = function(role) {
return function(req, res, next) {
if (req.user && req.user.role === role) next();
else
res.send(404);
}
}
In your route later
app.get('/profile', requireRole('member'), function(req, res) {
// do whatever you want to do
});
app.get('/dashbord', requireRole('operator'), function(req, res) {
// do whatever you want to do
});
There are a lot of ways to implement different access level to a user. This method is one of many.
The best solution would be to use schema inhertiance. That is why we use an ORM like mongoose.
var VehicleSchema = mongoose.Schema({
make : String,
}, { collection : 'vehicles', discriminatorKey : '_type' });
var CarSchema = VehicleSchema.extend({
year : Number
});
var BusSchema = VehicleSchema.extend({
route : Number
})
var Vehicle = mongoose.model('vehicle', VehicleSchema),
Car = mongoose.model('car', CarSchema),
Bus = mongoose.model('bus', BusSchema);
var accord = new Car({
make : 'Honda',
year : 1999
});
var muni = new Bus({
make : 'Neoplan',
route : 33
});
accord.save(function(err) {
muni.save(function(err) {
// vehicles are saved with the _type key set to 'car' and 'bus'
});
})
At this point in MongoDB you will have documents similar to this
{ "_type" : "car", "make" : "Honda", "year" : 1999, "_id" : ObjectId("5024460368368a3007000002"), "__v" : 0 }
{ "_type" : "bus", "make" : "Neoplan", "route" : 33, "_id" : ObjectId("5024460368368a3007000003"), "__v" : 0 }
Source
when querying
Vehicle.find({}, function(err, vehicles) {
console.log(vehicles[0]); // vehicles[0] instanceof Car === true
console.log(vehicles[1]); // vehicles[1] instanceof Bus === true
});
Checkout source / more examples by looking at briankircho little cheatsheet enter link description here

Categories

Resources