Mongoose and Node - Implementing Roles - Best practices - node.js

I am working on a token based application, and I want to apply roles for different types of user.
My question is if it is a better practice to create a model "Group" and in the model "User" and reference that model "Group" to the user_role property in "User" model.
It would be like this: (I haven't started yet so I can not copy real code, this is just for demonstrative purposes)
const GroupSchema = new Schema({
role: {
type: String,
enum: ['admin', 'employee', 'guest']
},
}
const UserSchema = new Schema({
role: {
type: Schema.Types.ObjectId,
ref: 'Group'
},
},
...
On the other hand I I thought doing this:
const UserSchema = new Schema({
role: {
type: String,
required: true,
},
}
...
But I don't really know what would be the advantages and disadvantages of using one or the other, regarding scalability basically, and if there is a better way.

Related

Is this the best way to design a mongodb schema?

I'm fairly new to programming, and am now at the stage where I am working on projects before starting to apply for jobs. I’m working on an express project and wanted some advice on my schema design, just in case I’m going about things in a horrible way.
The premise of the project is a desk booking app. A business administrator can create an account to register a company and add multiple offices if needed. Employees can also register as employees of a company using the unique company code and passcode, and then book desks in different offices.
This is the schema I have designed, I’ve modelled the data in this way as I want to be able to access the data in different ways e.g. an employee viewing all bookings they have made, as well as an employer viewing all bookings made in an office.
Any feedback would be much appreciated! Thanks in advance.
NB: When building the project I will have each schema in a different file, and I know I haven't required all the dependencies I will need. The purpose of this question is purely on best practices for mongoose schema design.
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const CompanySchema = new Schema({
companyName: String,
companyLogo: String,
uniqueCompanyCode: String,
companyPasscode: String,
companyAdmin: [
{
type: Schema.Types.ObjectId,
ref: "User"
}
],
offices: [
{
type: Schema.Types.ObjectId,
ref: 'Office'
}
],
})
const UserSchema = new Schema({
username: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
bookings: [
{
type: Schema.Types.ObjectId,
ref: 'Booking'
}
]
})
UserSchema.plugin(passportLocalMongoose);
const OfficeSchema = new Schema({
officeAddress:{
streetAddress: String,
town: String,
county: String,
postcode: String
},
floorPlan: String,
desks: [
{
deskNumber: Number,
bookings:{
type: Schema.Types.ObjectId,
ref: 'Booking'
}
}
]
})
const bookingSchema = new Schema({
bookedFrom: Date,
bookedTo: Date,
bookedBy: {
type: Schema.Types.ObjectId,
ref: "User"
}
})
If you’re using the architecture of Node.js, MongoDB with mongoose, you’ll create these schemas at the beginning of every project so it’s best to get familiar with them.
This is how we create a schema in mongoose.
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
let productSchema = new Schema({
name: {
type:String,
required:true
},
price: {
type:Number,
required:true
},
isAvailable: {
type:Boolean
}
});
const productModel = mongoose.model("product", productSchema);
module.exports = productModel;
The first parameter of the mongoose.model is the name of the collection that will contain the documents. The second parameter is the schema. Now, we need is to export productModel so they can be used elsewhere.
const productModel = mongoose.model("product", productSchema);
This productModel is ready to be imported and used to create data following our schema in the product collection. This is a common development pattern when using Node.js and MongoDB. An ODM-like mongoose makes managing your data more intuitive.

What's the best way of modeling mongoose/mongoDB database for saas

I'm trying to model a Saas application in which I'll have different companies using the services. I'm a bit confused on best way of modelling my mongoose database for this type of service.
So the model is something like
Company
Name
Users
->Roles (Admin,editor etc)
Projects
What's the best way of modeling such an application?
Should i create different models for user, projects etc and attach the company unique ID to all those users or projects that are within that company. Also Referencing those users or projects in the company model as well. Something like
//User is used as a separate model and is only referenced in company schema
var userSchema = new Schema({
Name : String,
Role: {
type: String,
enum: ['admin','reader', 'editor'],
default: 'reader'
},
Company : { type: Schema.ObjectId, ref: 'Company' }
});
mongoose.model('User', userSchema);
var companySchema = new Schema({
Name : String,
Users : [
{ type: Schema.ObjectId, ref: 'User' }
],
Projects : [
{ type: Schema.ObjectId, ref: 'Project' }
]
});
mongoose.model('Company', companySchema);
Or
Just referencing the user and project schemas in the company schema without having separate models
//userSchema is used as a child in company schema
var userSchema = new Schema({
Name : String,
Role: {
type: String,
enum: ['admin','reader', 'editor'],
default: 'reader'
},
});
var companySchema = new Schema({
Name : String,
Users : [
userSchema
],
Projects : [
projectSchema
]
});
mongoose.model('Company', companySchema);
Which is the best approach on the long run in terms of easily making queries for the sub-documents, memory allocation, public api exposure in future if required?
And if its the second way then do i need to have id's or those subdocuments? Can i just use the unique company id for making queries and then finding the stuff needed once the relative company is found within the document?
Nevermind, found a very detailed answer from the series of posts https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design-part-1

MongoDB complex associations

I am trying to figure out how to structure my mongoose models. I have a User model, a Task model, and Project model. Projects will have users, and each user will have tasks for that specific project. The way I have this set up is the User model has a schema reference to Project model and the Task model has a scheme reference to a user. How can I do this so that when I render the information retrieved, each project will show its relevant members and each members will have their relevant tasks for that particular project. I also have an admin property of the User model which is just a boolean set default to false. The purpose of this is so that when a user created a team, the Admin property will be set to True allowing the admin to set tasks for users in the project created by admin. The problem with this is, after a team create by the user, if the admin is set to true, the ternary condition on my front end that enables a form input to show based on the boolean value of the 'admin' property will show up for all projects, even for ones the user is not an admin of.
I am using React for the rendering.
//Tasks Models
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var TodoSchema = new Schema({
task: {
type: String
},
userTasks: [{
type: Schema.Types.ObjectId,
ref: "User"
}]
});
var Task = mongoose.model('Task', TodoSchema);
module.exports = Task;
//Users model
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var UserSchema = new Schema({
name: {
type: String,
trim: true,
required: "First Name is Required"
},
username: {
type: String,
trim: true,
required: "Username is Required"
},
skills: {
type: String,
trim : true
},
avatar: {
type: String
},
SQLid: {
type: Number
},
userCreated: {
type: Date,
default: Date.now
},
lastUpdated: {
type: Date
},
userAdmin: {
default: false
},
adminTeams: [{
type: Schema.Types.ObjectId,
ref: "Team"
}],
team: [{
type: Schema.Types.ObjectId,
ref: "Team"
}],
task: [{
type: Schema.Types.ObjectId,
ref: "Task"
}]
});
var User = mongoose.model("User", UserSchema);
// Export the model so the server can use it
module.exports = User;
//Projects model
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var TeamSchema = new Schema({
teamname: {
type: String,
trim: true,
required: "Team Name is Required"
},
description: {
type: String
},
tech: {
type: String
},
teamMembers: [{
type: Schema.Types.ObjectId,
ref: "User"
}]
});
var Team = mongoose.model('Team', TeamSchema);
module.exports = Team;
Once a User creates a team/project, they become an admin of that created team. Admins have the authority to assign task to Users, and Users can belong to many teams. I am thinking about moving the admin boolean to the Projects/Team model and giving that property the _id of the User once they create a team and then Ill use those as keys to match and use a ternary to render a form if the project they are viewing is one they created. However, I am still confused on how I can associate each user with a task, and users can belong to many teams, so I need the tasks that users have to be in the correct Project/Team section.
A lay of what I am talking about
//Projects Page (the div below is just one project out of many listed on the projects page
<div>
Project/Team 1
User Name -> User Task
User Name -> User Task
User Name -> User Task
...
</div>
<div>
Project/Team 2
User Name -> User Task
User Name -> User Task
User Name -> User Task
...
</div>
In general you should review the mongodb documentation on associations
When you're dealing with mongodb you may want to look into holding more associated information on your documents then you would in an sql environment. Theres a good top level answer here about designing your data to use mongodb well.
Your admin problem sounds like a coding issue that will not be solved through your db. I'm hard pressed to see why your form would need a ternary at all if it's displaying on a boolean field, but can't provide any further help then:
if (user.admin) {//form}
without snippets or information on your front end architecture or the ternary in question . It sounds like you may be overcomplicating things for yourself.
edit for your update: I would change your model so projects store the user who created them.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var TeamSchema = new Schema({
teamname: {
type: String,
trim: true,
required: "Team Name is Required"
},
description: {
type: String
},
tech: {
type: String
},
teamMembers: [{
type: Schema.Types.ObjectId,
ref: "User"
}],
// now holds the 'admin' of the team. you can keep the admin naming
//if you like
creator: [{
type: Schema.Types.ObjectId,
ref: "User"
}]
});
var Team = mongoose.model('Team', TeamSchema);
module.exports = Team;
also your user model could use some naming improvements, also an admin field on the user column is often used for application admins like yourself, as opposed to an admin inside a project.
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var UserSchema = new Schema({
name: {
type: String,
trim: true,
required: "First Name is Required"
},
username: {
type: String,
trim: true,
required: "Username is Required"
},
skills: {
type: String,
trim : true
},
avatar: {
type: String
},
SQLid: {
type: Number
},
// you might want to look up timestamps createdAt and updatedAt
// instead of this field
userCreated: {
type: Date,
default: Date.now
},
lastUpdated: {
type: Date
},
userAdmin: {
default: false
},
// this could be removed as its is now stored on the Team schema
adminTeams: [{
type: Schema.Types.ObjectId,
ref: "Team"
}],
// this should be plural as it represents a one to many
teams: [{
type: Schema.Types.ObjectId,
ref: "Team"
}],
// tasks is removed because you can find them from the Task schema
});
var User = mongoose.model("User", UserSchema);
// Export the model so the server can use it
module.exports = User;
Since your task model needs to be team specific it can look like this:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var TodoSchema = new Schema({
task: {
type: String
},
// I would suggest an 'assignedTo', 'owner', or 'taskOwner' naming
// as user may be a bit non-descriptive.
user: [{
type: Schema.Types.ObjectId,
ref: "User"
}],
team: [{
type: Schema.Types.ObjectId,
ref: "Team"
}]
});
var Task = mongoose.model('Task', TodoSchema);
module.exports = Task;
There are a lot of ways to load the information you want with the schema, but given this slight restructuring you could get all the information you need like this:
var tasks;
Team.findOne({'id': the_team_id}, function(err, team) {
if (err) return handleError(err)
Task.find({'team': team}).populate('user').exec(function(err, foundTasks) {
tasks = foundTasks
})
})
You can now check your admin by doing something along this line:
team.creator.id === user.id
mogoose populate

Mongo user document structure with three user types

I'm setting up a Mongo database in Express with Mongoose and I'm trying to decide how to model the users. I've never modeled multiple users in the MEAN stack before and thought I'd reach out for some best-practices - I'm an instructor and need to be able to teach my students best practices. I haven't been able to find a whole lot out there, but perhaps I'm searching for the wrong things.
The app will have 3 user types, student, staff, and admin. Each user type will require some of the same basics - email, password, first and last names, phone, etc. If the user is a student, they will need to provide additional info like their high school name, grade, age, gender, etc, which ideally will be required.
This is what I've come up with so far - a single user model that requires all the basic information, but also has schema set up to allow for the additional information that students will need to include. Then I also have a pre-save hook set up to remove the "studentInfo" subdocument if the user being saved doesn't have a "student" role:
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var ethnicityList = [
"White",
"Hispanic or Latino",
"Black or African American",
"Native American or American Indian",
"Asian / Pacific Islander",
"Other"
];
var userSchema = new Schema({
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
phone: {
type: Number,
required: true
},
email: {
type: String,
required: true,
lowercase: true,
unique: true
},
password: {
type: String,
required: true
},
preferredLocation: {
type: String,
enum: ["provo", "slc", "ogden"]
},
role: {
type: String,
enum: ["student", "staff", "admin"],
required: true
},
studentInfo: {
school: String,
currentGrade: Number,
ethnicity: {
type: String,
enum: ethnicityList
},
gender: {
type: String,
enum: ["male", "female"]
}
}
}, {timestamps: true});
userSchema.pre("save", function (next) {
var user = this;
if (Object.keys(user.studentInfo).length === 0 && user.role !== "student") {
delete user.studentInfo;
next();
}
next();
});
module.exports = mongoose.model("User", userSchema);
Question 1: Is this an okay way to do this, or would it be better just to create two different models and keep them totally separate?
Question 2: If I am going to be to restrict access to users by their user type, this will be easy to check by the user's role property with the above setup. But if it's better to go with separated models/collections for different user types, how do I check whether its a "Staff" or "Student" who is trying to access a protected resource?
Question 3: It seems like if I do the setup as outlined above, I can't do certain validation on the subdocument - I want to require students to fill out the information in the subdocument, but not staff or admin users. When I set any of the fields to required, it throws an error when they're not included, even though the subdocument itself isn't required. (Which makes sense, but I'm not sure how to get around. Maybe custom validation pre-save as well? I've never written that before so I'm not sure how, but I can look that up if that's the best way.)
Well, Here are my two cents.
You would be better off creating separate schema models and then injecting the models on a need to basis.
for e.g.
If I have a blog schema as follows:
var createdDate = require('../plugins/createdDate');
// define the schema
var schema = mongoose.Schema({
title: { type: String, trim: true }
, body: String
, author: { type: String, ref: 'User' }
})
// add created date property
schema.plugin(createdDate);
Notice that author is referring to User and there is an additional field createdData
And here is the User Schema:
var mongoose = require('mongoose');
var createdDate = require('../plugins/createdDate');
var validEmail = require('../helpers/validate/email');
var schema = mongoose.Schema({
_id: { type: String, lowercase: true, trim: true,validate: validEmail }
, name: { first: String, last: String }
, salt: { type: String, required: true }
, hash: { type: String, required: true }
, created: {type:Date, default: Date.now}
});
// add created date property
schema.plugin(createdDate);
// properties that do not get saved to the db
schema.virtual('fullname').get(function () {
return this.name.first + ' ' + this.name.last;
})
module.exports = mongoose.model('User', schema);
And the created Property which is being refereed in both User and Blogspot
// add a "created" property to our documents
module.exports = function (schema) {
schema.add({ created: { type: Date, default: Date.now }})
}
If you want to restrict access based on the user types, you would have to write custom validation like in the User schema we had written for emails:
var validator = require('email-validator');
module.exports = function (email) {
return validator.validate(email);
}
And then add an if-else based on whatever validations you do.
2 and 3. So, Yes custom validations pre-save as well.
Since you are an instructor I preferred to just point out the practices that are used instead of elaborating on your specific problem.
Hope this helps! :)

MongoDB performances $ref vs embedded

I recently started a project using mongodb and nodejs to build a restful web service. Unfortunately mongodb is very new to me, and coming from the relational databases world I'm asking my self a lot of questions.
Let me explain you my problem :
The goal is to build a sort of content management system with social features like a user can post topics that can be shared and commented.
I have 2 possibilities to do this the one using a reference to get topics posted by a user, the second using topics as embedded document of user instead of reference.
So basically I can have these 2 schemas :
var UserSchema = new Schema({
username: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
name: {
type: String
},
first_name: String,
phone: String,
topics: [Topic.schema]
});
var TopicSchema = new Schema({
_creator: {
type: String,
ref: 'User'
},
description: String,
comments: [Comments.schema],
shared_with: [{
type: Schema.ObjectId,
ref: 'User'
}] //[{ type: String, ref: 'User'}]
});
var CommentSchema = new Schema({
_creator: {
type: String,
require: true
},
text: {
type: String,
required: true
},
});
and
var UserSchema = new Schema({
username: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
name: {
type: String
},
first_name: String,
phone: String,
topics: [{ type: Schema.ObjectId, ref: 'Topics'}]
});
var TopicSchema = new Schema({
_creator: {
type: String,
ref: 'User'
},
description: String,
comments: [Comments.schema],
shared_with: [{
type: Schema.ObjectId,
ref: 'User'
}] //[{ type: String, ref: 'User'}]
});
var CommentSchema = new Schema({
_creator: {
type: String,
require: true
},
text: {
type: String,
required: true
},
});
So the first schema uses 1 collection of user document and the second use 1 collection for the user and 1 collection for the topics, this implies to make for example, 2 finds queries to retrieve a user and it's topics but it is also easyer to query directly the topics.
Here is the request I use to retrieve a specific topic with some user info with the first schema :
User.aggregate([
{$match: {
"topics._id":{$in:[mongoose.Types.ObjectId('56158c314861d2e60d000003')]}
}},
{ $unwind:"$topics" },
{$match: {
"topics._id":{$in:[mongoose.Types.ObjectId('56158c314861d2e60d000003')]}
}},
{ $group: {
_id: {
_id:"$_id",
name:"$name",
first_name:"$first_name"
},
topics:{ "$push": "$topics"}
}}
]);
So the question is, what do youh think ? Which is the good schema in your opinion ?
Thanks in advance.
Better solution: using a reference to get topics posted by a user
For this database use, one typically needs to consider the MMAPV1 document size limit (16MB). Putting user, topic, and comments in one document allows the document to grow without bound. If each topic is a page of text (1K), then each user could have about 16,000 topics before the limit is reached. That seems huge, but what happens if you decide to put images, videos, sounds in the topic as the product matures? Converting from an embedded to a normalized schema later would be a lot more work than a simple design choice today.
Similarly, if the comments could grow to cause a topic to exceed the 16MB limit, they should be in a separate collection. Unlikely? Probably. But if you are writing something that will become, say, the Huffington Post - check out comments on their popular articles.
Here is mongo's advice on data model design

Resources