MongoDB / Mongoose schema for users - mongodb

I start by saying I'm a beginner with mongodb, but I'm developing a webapp just to study this db in more depth.
I can't "think" of a valid solution (at the schema level), which allows me to manage users and the data associated with them in this case:
I have three types of users: "basic user", "user manager" and "supervisor".
The basic user must be able to see only his data, the “user manager“ must be able to see his data and the data of the users under him. In cascade, the supervisor user must be able to see his data, those of the manager and users under him.
-Practical example:
a user "X" has been assigned tasks, if user "X" has a manager "R", the manager must see his tasks and the tasks of "X". In cascade, if the manager has a supervisor, the supervisor must be able to see his tasks, those of "R" and those of "X."
Any advice or example?
Many thanks

You can create one collection to store the user details.
Example - UserSchema
email: {
type: String,
unique: true,
required: true
},
userId: {
type: String,
unique: true,
required: true
},
managerId: {
type: String,
required: false
},
superVisorId: {
type: String,
required: false
},
name: {
type: String
},
roleId: {
type: Array,
required: true,
enum: [EMPLOYEE, MANAGER, SUPERVISOR],
default: [EMPLOYEE]
}
Here userId is something which you can assign uniquely to each user.
Now you can write some middleware which checks the roleId when a user is trying to access data of say -> EMPLOYEE is trying to access data of MANAGER based on the roleId you can reject this action.
You can have another collection taskSchema
taskId: {
type: String,
required: true,
unique: true
},
taskDescription: {
type: String
},
// Store the userId from userSchema,
// This refers to whom this particular task is assigned to.
taskAssignedTo: {
type: String,
required: true,
unique: true
}
Now, If the SUPERVISOR want's to access all the tasks that have been assigned to them You can simply run a query to first fetch all the MANAGERS and then fetch all the EMPLOYEE under that manager and store them.
Now just fetch all the tasks assigned to each of these userIds.

Related

Database design - saving the entire object to a user or just the id of an object?

database noob here using MongoDB, in my program, I have users, and the core of my program are these roadmaps that I display. So, each user can create roadmaps, save others roadmaps, blah blah... Each user has a field named savedRoadmaps and createdRoadmaps which should store the roadmaps. My question is, should I just store the roadmap _ids in the savedRoadmap and createdRoadmaps field or the entire roadmap?
I am asking this because it feels like saving just the _id of the roadmaps can save storage, but it might not come in handy when I have to fetch the data of the user first, then fetch the roadmap using the roadmap ID in the user's savedRoadmap/createdRoadmap field, versus just fetching the user and the savedRoadmap field will already have the roadmap in there.
And btw, is there any sweet and brief database design read out there, please direct me to some if you know any!
For a user, I want it to have a name, email, password, description ofcourse, and also savedRoadmaps and createdRoadmaps. A user can create unlimited roadmaps and also save as much as he or she wants. For a roadmap, I want it to have a name, category, time_completion, author, date, and a roadmap object which will contain the actual json string that I will use d3 to display. Here's my User and Roadmap Schema right now:
const RoadmapSchema = new Schema({
author: {
type: String,
require: false
},
name: {
type: String,
require: true
},
category: {
type: String,
require: true
},
time_completion: {
type: Number,
require: true
},
date: {
type: Date,
default: Date.now
},
roadmap: {
type: "object",
require: true
}
});
and User Schema:
const UserSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
},
savedRoadmap: {
type: "object",
default: []
},
createdRoadmap: {
type: "object",
default: []
}
});
My question is, inside of the savedRoadmap and createdRoadmap fields of the User schema, should I include just the _id of a roadmap, or should I include the entire json string which represents the roadmap?
There are 3 different data-modeling techniques you can use to design your roadmaps system based on the cardinality of the relationship between users and roadmaps.
In general you need to de-normalize your data model based on the queries that are expected from your application:
One to Few: Embed the N side if the cardinality is one-to-few and there is no need to access the embedded object outside the context of the parent object
One to Many: Use an array of references to the N-side objects if the cardinality is one-to-many or if the N-side objects should stand alone for any reasons
One-to-Squillions: Use a reference to the One-side in the N-side objects if the cardinality is one-to-squillions
And btw, is there any sweet and brief database design read out there,
please direct me to some if you know any!
Rules of Thumb for MongoDB Schema Design: Part 1

Cannot set the reference to the user document in courses mongoose

I am currently working on a RESTful API, and I am trying to reference the users schema in the courses document such that, when a POST request gets sent to the route of the course, a course is created in the DB and has as one of its fields a reference to the user that created it. However, for the life of me, I cannot figure out why the "user" field is not appearing when I post. There seem to be quite a few of these questions here on Stack so I may just be adding to the pile, but I tried their solutions and they did not work for me
var mongoose = require('mongoose')
var Schema = mongoose.Schema
var userSchema = new Schema({
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
emailAddress: {
type: String,
required: true
},
password: {
type: String,
required: true
}
});
var CourseSchema = new Schema({
user: {type: Schema.Types.ObjectId, ref: 'User'}, //FOR some reason this is not showing up on any courses created using the
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
estimatedTime: {
type: String
},
materialsNeeded: {
type: String
}
});
var User = mongoose.model('User', userSchema);
var Course = mongoose.model('Course', CourseSchema);
module.exports = {Course, User};
Do you see anything in here that would preclude the user field from appearing when a new course is created?
I have attached some screenshots to further explain.
This first image is a screen of the currently authenticated user credentials (fake data obviously). This is the user that is sending the POST request for the new course. I would expect his information to be attached to the course (see screenshot 3)
This image shows the body of the request that is sent. You can see that the key-value pairs match what is in the CourseSchema. I would expect that the "user" field would be created once the POST request is sent.
This last image is some dummy data that is the expected result.
Thanks all for taking a look at this!
User field will not be automatically added to the course document. You have to manually set the user field in the request body itself or while creating a course.
Example of the course body to be sent:-
{
user: "userId",
title: "test",
description: "test",
estimatedTime: "test",
materialsNeeded: 1
}
Also, the result of this will not include the whole user document as you have mentioned in the expected result. It will only return the userId. However, while accessing the course you can populate the user field to get the whole user document. Example for the same
Course.find({...query}).populate("user")

sailsjs one-way associations confusion

I am having a hard time wrapping my head around associations with sailsjs.
I have 2 models
Services
attributes: {
status: {
defaultsTo: 'inactive'
},
userId:{
model: 'users',
via: 'id',
},
},
Users
attributes: {
email: {
type: 'string',
required: true,
unique: true
},
password: {
type: 'string'
}
},
So, a service is tied to a user (matching the id of the user).
I used to do a call like http://localhost:1337/Services?userId=userId
Now I would like to transition to associations using the above model attributes.
This works by calling the ID of the service just fine (it includes the users data as well), however if all i have is the user, how could I get the service
Doing the same call (http://localhost:1337/Services?userId=userId) returns and empty object.
Am I forced to actually have a one-to-one or one-to-many association? I don't understand why I can no longer use the userId field (stored in the DB) to do queries once I start using associations. I guess I am looking for the best of both worlds here.
EDIT:
Let me try make this more clear. Before trying to do associations, I could call this URL (using blueprint)
http://localhost:1337/Services?userId=userId
The Services model used to look like this
attributes: {
status: {
defaultsTo: 'inactive'
},
userId:{
type: 'string',
required: true,
},
},
Then when a user is created, a service for that user is created with the userId matching the ID in the Users table.
Now I would like to implement associations using the above model scheme.
However, because (my best guess) the userId field of the service is mapped to the Users model, I am unable to search for a Server using the userId field that is stored.
I hope that makes sense? In another words, tryin to call
http://localhost:1337/Services?userId=userId
returns nothing when using associations but does return a value when I don't use associations

mongoose dynamically attaching schema to validate against on the fly

I'd like to dynamically attach a schema to a particular field, based on some business logic in schema.pre('save',function(..){...}). How to do this, if at all possible?
Some (simplified) schemas and background:
var fact= new Schema({
name: { type: String, required: true, index: { unique: false }}
,value: {type: {}, required: true}
,moreinfodesc: { type: String, required: false}
,createddate : { type: Date, required: true, default: Date.now, select: false } }
}, { collection: 'Fact' } );
var factSchema= new Schema({
name: { type: String, required: true, index: { unique: true }}
, valueType: { type: {}, required: true}
,isMulti: {type: Boolean, required: true }
//ACL-stuff
,directChangeRoles: {type: [String]} //i.e: [super, admin,owner]
,suggestChangeRoles: {type: [String]} //ie: [editor]
,enum: {type: [{}]}
,mixins: {type: [String]}
}, { collection: 'FactSchema' });
This is a simplified structure to allow "facts' of a particular 'entity' to be edited.
e.g: entityA.facts=[fact]
As can be seen from the schema's fact.value can have any type as far as mongoose is concerned. I however, want to constrain it at runtime to the schema as defined in
FactSchema.valueType (a String containing "Boolean", "String" or something more complex as "[Tag]") . This might all seem cumbersome, but this is the way i'd like to go for several reasons.
So let's say that for a particular fact with fact.name=tags I want to assign fact.value the type [Tag] at runtime. For this I would have setup a Tag-schema with validation like usual and have fact.value validate against it.
I was thinking of somehow "attaching" the [Tag]-schema to fact.value in fact.pre('save',function(..){.. //validation here }) and hope validation would magically happen as if fact.valuewas assigned the type [Tag] at design-time instead of run-time.
Finally the question: I've got no idea if it's possible to do that 'attach' and if so, how?
Thanks.
"attaching" at run time isn't possible but you can add a custom validator to your path and base its logic on current document state:
https://gist.github.com/2789681
Try using mongoose discriminators
And if needed, you can alter the validation at runtime with:
YourModelName.schema.path('your_field_name').required(false);

Mongoose / MongoDB User notifications scheme suggestions

I was wondering what is the best scheme for user/notifications kind of scenario like the following :
You have multiple users.
You have multiple notifications that might be for a single user, for some users or for all users.
You need a notification "read" entry in the storage, to know if the user has read the notification or not.
Option One
Embedded notifications scheme
Notifications = new Schema ( {
message : String,
date : { type : Date, default: Date.now() }
read : { type: Boolean, default : false }
});
User = new Schema( {
username : String,
name : String,
notifications : [Notifications]
});
Pros :
It is very easy to display the data, since calling User.find() will display notifications as array object.
Cons :
When you create a notification for every user, you need to do .push to every embedded Notifications
Multiple notifications entries for every user ( multiple data in the database )
Giant embedded document ( I read something about <4MB limit of those )
Since it is a Embedded Document - ( mongoose DocumentArray ) you can't search or skip. You load every notifications everytime you access user.
Option Two
Populate (DBRef like) objects
Notification = new Schema ({
message : String,
date : { type : Date, default : Date.now() }
});
UserNotification = new Schema ({
user : { type : Schema.ObjectId, ref : 'User' },
notification : { type : Schema.ObjectId, ref : 'Notification' },
read : { type : Boolean, default : false }
});
User = new Schema( {
username : String,
name : String,
notifications : [ { type : Schema.ObjectID, ref : 'UserNotification' } ]
});
Pros :
Optimal for queries
No duplicate data
Supporting large amount of notifications
Cons :
You have 3 collections, instead of one in ( Option One has only one collection )
You have 3 queries every time you access the collection.
Questions
What do you think is the best scheme from those two?
Am I missing something or some kind of basic NoSQL knowledge?
Can someone propose better scheme?
Thank you, in advance and I'm sorry for the long post, but I think I can't explain it simpler.
Option 1 looks like it will probably result in a lot of excessive document growth and moves, which would be bad for performance, since most of your writes will be going to the embedded doc (Notifications).
Option 2 I'm not totally clear on your strategy - it seems redundant to have those 3 collections but also embed a list of notifications by objectId if you are already referencing user by ID in the notifications table. You could index on user in Notifications table, and then eliminate the nested array in the Users table.
(EDIT)
Here's another strategy to consider.
Three collections that look like this:
Users:
_id: objectid
username : string
name: string
Notifications:
_id: objectid
to (indexed): objectid referencing _id in "users" collection
read: boolean
Global Notifications:
_id: objectid
read_by: [objectid referencing _id in users]
For notifications meant for a single user, insert into Notifications for that user. For multiple users, insert one for each user (alternately, you could make the "to" field an array and store _ids of all the recipients, and then maintain another list of all the recipients who have read it). To send a notification to all users, insert into Global Notifications collection. When the user reads it, add their user's _id to the read_by field.
So, to get a list of all the unread notifications of a user, you do two queries: one to notifications, and one to global notifications.
// namespace like a channel
// we have a notification from specific channel or namespace
const NamespaceSchema = new Schema({
name: {
type: String,
unique: true,
required: true
},
author: {
type: String,
required: true
},
createdAt: {
type: Boolean,
default: Date.now()
},
notifications: [{
type: Schema.Types.ObjectId,
ref: 'Notification',
}]
});
// the notification schema have a subscribers to specific notification (objectId)
//
const NotificationSchema = new Schema({
title: {
type: String
},
message: {
type: String
},
read: {
type: Boolean,
default: false
},
subscribers: [{
type: Schema.Types.ObjectId,
ref: 'Subscriber'
}],
createdAt: {
type: Date,
default: Date.now()
}
});
// subscribers subscribe to a namespace or channel
const SubscriberSchema = new Schema({
subscriber: {
type: String,
required: true
},
namespaces: [{
type: Schema.Types.ObjectId,
ref: 'Namespace',
}]
});