Mongo user document structure with three user types - node.js

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! :)

Related

Fetch the Username from another mongoose library efficiently

how can I fetch the Users Schema username easily when rendering a table which displays all products and for each product the user who owns the product (username).
For some reason, I get back nothing instead of the return user.username.
When rendering 1000 products into a table and asking from the database the User schema 1000 times is pretty inefficient. What's a better approach for it? I'm using handlebars to render the table and mongoose to query the data.
User model:
const UserSchema = new Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
}
});
Product model:
const ProductSchema = new Schema({
name: {
type: String,
required: true
},
amount: {
type: Number,
required: true
},
ownerID: {
//The ID of Users model
type: String,
required: true
}
});
Handlebars code (Table row part. Im looping trough all my products):
<td>{{returnProductsOwner this.ownerID}}</td>
<td>{{this.name}}</td>
<td>{{this.amount}}</td>
ReturnProductsOwner:
returnProductsOwner: function(id) {
User.findById(id).then((user) => {
return user.username;
}).catch();
}
As you are using no-sql database why are you saving user details separately? No-sql db doesnt handle joins very well. So what you can do is store the User details with the product details itself so you will be able to query it faster and efficently.

Add Data to MongoDB Models At Different Times

I have a pretty good understanding of mongdoDB with mongoose, but this one aspect of it has been confusing me for a while now. I have a user.js model with a username, password, etc (all the basic user stuff). This data is added when a user registers for an account. But each user also has more data linked to it that IS NOT created or added at the time of registering.
This is my model:
// User Schema
const UserSchema = new Schema({
// PERSONAL USER INFO
username: {
type: String,
index: true
},
email: {
type: String
},
password: {
type: String
},
// INSTAGRAM ACCOUNT INFORMATION
ig_username: {
type: String
},
ig_password: {
type: String
},
story_price: {
type: Number
},
fullpost_price: {
type: Number
},
halfpost_price: {
type: Number
},
leads: [{
title: { type: String }
}]
});
// EXPORTS
const User = module.exports = mongoose.model('user', UserSchema);
All the field except "leads" are created at the time of registering. But I want to fill the Leads field using another form. I've tried the .update(), .save(), $set, $push, and all kinds of methods, but I cannot get it to work.
Most solutions that I have found use var user = new User({...}) to create a new user and then use .save() after adding the additional data. But this seems wrong since the user has already been created and I am just trying to add data to an additional field.
I think I'm just glossing over something basic, but if there is a way to do this I would be glad to hear it. Thanks!
I would create a sub-schema for leads
// Create a sub-schema for leads
const leadsSubSchema = new Schema({
title: {
type: String,
},
});
// Create a schema for user
const UserSchema = new Schema({
username: {
type: String,
index: true
},
// ...
leads: [leadsSubSchema]
});
// EXPORTS
const User = module.exports = mongoose.model('user', UserSchema);
Then for the update
User.update({
_id: user_id,
}, {
$push: {
leads: lead_to_add,
},
});

Friend Request System with Express & MongoDB

I am trying to let users send friend requests to other users similar to that of Facebook and other social media platforms. I have started creating this functionality, but quickly got stuck since I am new to mongoDB and the whole Schema thing.
Here are the models I currently have:
// User Schema
var UserSchema = new Schema({
_id: {
type: Number
},
name: {
type: String
},
username: {
type: String,
index: true
},
password: {
type: String,
required: true
},
email: {
type: String
},
friends: [{
friendName: {
type: Schema.ObjectId,
required: true,
ref: 'User'
},
duration: {
type: Number
}
}]
});
// Friendship Schema
var FriendshipSchema = new Schema({
participants: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
requestTo: {
type: Schema.Types.ObjectId,
ref: 'User'
},
accepted: {
type: Boolean,
default: false
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
var Friendship = module.exports = mongoose.model('Friendship', FriendshipSchema);
var User = module.exports = mongoose.model('User', UserSchema);
This is as far as I have gotten. From here, I do not know how to use these schemas to establish friendships between 2 users. My ultimate goal is to have a button on a webpage that sends a friend request to the intended recipient, where they can then accept or deny the request.
Any help with this would be awesome, since I do not know what to do from here with these 2 schemas. Thanks!
We would need to take one schema only which is userSchema(as is Israel said above, you only need an array/object to list your friendship on the userSchema). But we will need to add another schema(said it friendRequestSchema).
FriendRequest schema would be:
- ID user request (int)
- ID user recipient (int)
- status (int) //let say 1= requested, 2=accepted, 3=rejected
And the controller it should be from the user A click "Friend Request" button on your user B page.
Friend Request Button will call a function let saying it "sendFriendRequest()"
If function running it would be recorded on friendRequest DB, which is will record ID of user A(as requester), ID of user B and request status.
If request status = 1 then user B will be notified and give him two option which is accepted and rejected.
User B accept or reject it
If user press button accept, then the status updated in friendRequest DB to be=> 2 (Accepted). Then, you have to call another function to add user ID A to friendship list of User B. Conversely. Then if you want to make a notification you can call it as well.
Else user B will press reject (status will be => 3) then notif it.
UserSchema
var UserSchema = new Schema({
name: String,
username: {
type: String,
index: true
},
password: {
type: String,
required: true
},
email: String,
friendship: [String] });
Then FriendRequestschema
var FriendRequestSchema = new Schema({
requester: {
type: int,
required: true
},
recipient: {
type: int,
required: true
},
status:
type: int,
required: true });
This just to let you know, how its work. More complex method about (sendrequest,block .etc) you can check this link, It's flow process for PHP, but you can easily move it to your js. Hope it help you man.
Your model can be improved, and your code can be cleaned:
First, you don't need the brackets if you only give type for the field:
var UserSchema = new Schema({
name: String,
username: {
type: String,
index: true
},
password: {
type: String,
required: true
},
email: String,
friends: [String]
});
This should be a simplified version of your schema. The _id field doesn't need to be specified because mongoose creates it automatically. If you wanna put a customized value there, just do it when you insert.
Second:
If you wanna reference other users, why not to use only a simple array that contains ids from other users. For example, if you have user A, the "friendship" of this user are user ids contained in his "friends" field.
{id:12345, username:"A", password:***, email:"a#fakemail.com", friends:[B_id,C_id,D_id]}
In that case, whenever you wanna make a list of friends of A, you can just perform a $lookup operation in mongodb and it will fill the other users information for you.
I don't think I covered all of your questions, but I hope my answer was helpful.

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

Connecting Mongoose Schema to one another

I have an existing schema for a user profile that looks like:
const userSchema = new mongoose.Schema({
email: { type: String, unique: true, lowercase: true },
password: { type: String },
name: {
first: { type: String, required: true },
last: { type: String, required: true }
}
});
My question is: can I create another schema which holds properties for event registration (such as type of pass the user is paying for, address, contact number, etc.) specific info instead and have it connect to the userSchema? Or would it be more effective to throw all of that info into the userSchema?
Example Registration schema:
const registrationSchema = new mongoose.Schema({
pass: { type: String },
address: { type: String },
phone: { type: String },
paid: { type: Boolean, default: false },
shirt: { type: Boolean, default: false }
});
Thanks.
There are three cases
The relation between entities is 1:1 (Your case?) - Put them in the same schema. We're lucky not to use relational db.
The relation is 1:N but there will be a predictable and not large amount of children per parent - also put them in the same schema.
Examples of 2:
a. user roles.
b. user visited countries.
the relation is 1:N and there can be any number of children per parent and it is not predictable - Use a different schema.
examples of 3:
a. user orders.
b. user location history.

Resources