Mongoose bidirectional models / many to many relationships - node.js

Fairly new to mongoose so I'm looking for the best way to do this. I have two models: Users and Companies. A user can have many companies and a company can have many users.
Right now my Company Schema looks like this:
CompanySchema = new Schema
name:
type: String
unique: true
slug:
type: String
users: [
type: Schema.Types.ObjectId
ref: 'User'
]
And then I can use .populate('users') to get a list of associated users. No problem there.
What I'm wondering is, what is the most efficient/best way to get all companies associated with a user? I don't want to do the same as above as the lists could get out of sync. Is there a way that when I update a company's user that the user's companies are also updated?
Ideally, I could just do User.find().populate('companies')

I haven't found a way to get populate to work both ways.
Normally we end up doing something like this
/**
* Load the categories for this account
* #method categories
* #memberof Account
* #instance
* #param {Function} done
*/
AccountSchema.methods.categories = function (done) {
var Category = this.model('Category');
return Category.find({account: this}, done);
};

Related

How can I count the view of a Specific post by a User ? Count every User just Once

We have user and news model, in the news model we have e viewsCount field, I want to increment this view count when a GET request is made by a User.
When a specific user makes a GET request, the view count will increment one, every user just one view.
const NEWSModel = new Schema({
viewesCount: { type: Number },
Publisher: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
LikesCount: { type: Number },
DislikeCount: { type: Number },
Comments: CommenTs
});
Every user can view the news as many times as wants, but just can make one view. How can I do that?
you Can change your model like and then whenever you get a news just push the user id to the viewedBy field.
news.viewedBy.push(user id)
viewedBy: [{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}]
}); ```
If you have not a lot of users, you can add additional field to news model like users_viewed which would be array of unique user ids.
And make additional check before incrementing views count.
If user, who requested news is already in this users_viewed array, you skip any additional actions.
If don't, increment views counter.
But if you do have a lot of users, it's better to store views counter in Redis to skip request to database and increment in memory counter.
The logic for storing and showing data would be the same, but you'll reduce load on your database and speed up the whole process.
[UPDATE] According to your comment, about number of users.
To make things work you can use this package.
First of all, after request for a news from a client, you can store all the news data in your cache (to reduce number of requests to your database).
Now you have few possible ways to handle number of views.
I think, the easiest to implement would be to add user unique identifier to SET. And return number of users in SET using SCARD;
In this solution you wouldn't need to check if user already watched the news, because set data structure holds only unique values (the same reason why do we need to use user's unique identifier).
And you just use 2 redis requests, which is pretty good for heavy load services.
You can have another field called viewedBy of type array in which you can store users ids. Then it will be easier to check if a user already viewed your post or to count them.
File: news.model.js
const News = new Schema({
viewedBy: [{
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}],
// other properties...
});
File: news.controller.js
const user = User.find({...}); // get current user
const news = News.find({...}); // get a news
/*
Update views count by adding the current user id if it's not already added
Thanks to '$addToSet', the update() function will do nothing if the user id it's already there)
*/
news.update({ $addToSet: { viewedBy: user._id } });
// Getting the views count
console.log('Total views:', news.viewedBy.length);
More about $addToSet: https://docs.mongodb.com/manual/reference/operator/update/addToSet/

Mongoose: ref custom field name

I am configuring Mongoose to work on an existing MongoDB, that has these two collections:
Users - with fields:
_id: ObjectId
name: String
org_id: ObjectId
Organizations - with fields:
_id: ObjectId
name: String
I want to be able to populate a User document by Organization data.
So I've created these two Models:
const userSchema = new Schema({
name: String,
org_id: {
type: Schema.Types.ObjectId,
ref: 'Organization',
},
});
const User = mongoose.model('User', userSchema);
const organizationSchema = new Schema({
name: String,
code: String,
});
const Organization = mongoose.model('Organization', organizationSchema);
Since historically the ref field from User to Organization is called org_id (instead of just organization) the population of a user by the organization code is:
const user = await User.findById('5b213a69acef4ac0f886cdbc')
.populate('org_id')
.exec();
where user.org_id will be populated by Organization data. Of course I would be happier to have organization instead of org_id in both - populate method and the path (i.e. user.organizationd).
What is the proper way to achieve it without changing the existing documents?
I could create my Schema methods (instead of populate) and aliases, but I am looking for a more generic and elegant solution.
I understood that you don't want to change the existent documents, but for me, if this name of field doesn't make more sense you need to refactor.
Change the name of the field, organization instead of org_id.
For this you can use the $rename command: MongoDB $rename
db.getCollection('users').updateMany({},{$rename: { "org_id": "organization" }});
After this you will can call .populate('organization').
If it is impossible, I believe that you will not find a solution better than aliases.
Mongoose Documentation: Aliases
I will follow along your code.looks like you applied this: mongoose.Schema=Schema
you embedded Organization model into User. first lets extract organization details for each user.
//import User and Organization models
const main=async ()=>{
const user=await User.findById("placeUserId")//we get the user
const populated=await user.populate("org_id").execPopulate()//we populated organization with all properties
console.log(populated.org_id) }
in the above code, org_id was already referenced in the userSchema. we just reached org_id property and extracted. this was simple. next without changing any code in userSchema and organizationSchema i will find which user is in which organization with virtual property.
virtual property allows us to create virtual fields in the database. it is called virtual because we do not change anything. it is just a way that to see how two models are related.
for this we are gonna add a little code on the page where you defined you defined your organizationSchema file which i assume in models/organization.js. this code will describe the virtual field. it is kinda schema of the virtual field.
//models/organization.js
organizationSchema.virtual('anyNameForField',{
ref:"User", //Organization is in relation with User
localField:"_id"//field that Organization holds as proof of relation
foreignField:"org_id"//field that User holds as proof of relation
})
now time to write the function to find the user inside the organization.
const reverse=async ()=>{
const organization=await Organization.findById("")
const populated=await organization.populate("anyNameForField").execPopulate()
console.log(populated.anyNameForField) //i gave a stupid name to bring your attention.
}
very simple and elegant!

Mongoose: How to find documents by sub-collection's document property value

I’m using Mongoose version 4.6.8 and MongoLab (MLab). I have a Mongoose schema called “Group” that has a collection of User subdocuments called “teachers”:
var GroupSchema = new Schema({
//…more properties here…//
teachers: [{
type: Schema.ObjectId,
ref: 'User'
}]
});
This is a document from the “groups” collection on MongoLab:
{
//…more properties here…//
"teachers": [
{
"$oid": "5799a9c759feea9c208c004c"
}
]
}
And this is a document from the “users” collection on MongoLab:
{
//…more properties here…//
"username": "bob"
}
But if I want to get a list of Groups that have a particular teacher (User) with the username of “bob”, this doesn’t work (the list of groups is empty):
Group.find({"teachers.username": "bob"}).exec(callback);
This also returns no items:
Group.find().where('teachers.username').equals('bob').exec(callback);
How can I achieve this?
Without some more knowledge of your set up (specifically whether you want anybody named Bob or a specific Bob whose id you could pick up first) - this might be some help although I think it would require you to flatten your teachers array to just their ID's, not single-key objects.
User.findById(<Id of Bob>, function(err, user){
Group.find({}, function(err, groups){
var t = groups.map(function(g){
if(g['teachers'].indexOf(user.id))
return g
})
// Do something with t
})
})
You can use populate to do that.
Try this:
Group.find({})
.populate({
path : 'teachers' ,
match : { username : "bob" }
})
.exec(callback);
populate will populate based on the teachers field (given path) and match will return only those who have username bob.
For more information on mongoose populate options, Please read Mongoose populate documentation.
I think the solution in this case is to get a teacher’s groups through the User module instead of my first inclination which was to go through the Groups module. This makes sense because it is in line with how modern APIs represent a one-to-many relationship.
As an example, in Behance’s API, an endpoint for a user’s projects is:
GET /v2/users/user/projects
And a request to this endpoint (where the User’s username is “matiascorea”) would look like this:
https://api.behance.net/v2/users/matiascorea/projects?client_id=1234567890
So in my case, instead of finding the groups by teacher, I would need to simply find the User (teacher) by username, populate the teacher’s groups, and use them:
User.findOne({username: 'bob'})
.populate('groups')
.exec(callback);
And the API call for this would be:
GET /api/users/user/groups
And a request to this endpoint would look like this:
https://example.com/api/users/bob/groups

Easy way to reference Documents in Mongoose

In my application I have a User Collection. Many of my other collections have an Author (an author contains ONLY the user._id and the user.name), for example my Post Collection. Since I normally only need the _id and the name to display e.g. my posts on the UI.
This works fine, and seems like a good approach, since now everytime I deal with posts I don`t have to load the whole user Object from the database - I can only load my post.author.userId/post.author.name.
Now my problem: A user changes his or her name. Obviously all my Author Objects scattered around in my database still have the old author.
Questions:
is my approuch solid, or should I only reference the userId everywhere I need it?
If I'd go for this solution I'd remove my Author Model and would need to make a User database call everytime I want to display the current Users`s name.
If I leave my Author as is, what would be a good way to implement a solution for situations like the user.name change?
I could write a service which checks every model which has Authors of the current user._id and updates them of course, but this sounds very tedious. Although I'm not sure there's a better solution.
Any pro tipps on how I should deal with problems like this in the future?
Yes, sometime database are good to recorded at modular style. But You shouldn't do separating collection for user/author such as
At that time if you use mongoose as driver you can use populate to get user schema data.
Example, I modeling user, author, post that.
var UserSchema = new mongoose.Schema({
type: { type: String, default: "user", enum: ["user", "author"], required: true },
name: { type: String },
// Author specific values
joinedAt: { type: Date }
});
var User = mongoose.model("User", UserSchema);
var PostSchema = new mongoose.Schema({
author: { type: mongoose.Scheam.Types.ObjectId, ref: "User" },
content: { type: String }
});
var Post = mongoose.model("Post", PostSchema);
In this style, Post are separated model and have to save like that. Something like if you want to query a post including author's name, you can use populate at mongoose.
Post.findOne().populate("author").exce(function(err, post) {
if(err)
// do error handling
if(post){
console.log(post.author.type) // author
}
});
One solution is save only id in Author collection, using Ref on the User collection, and populate each time to get user's name from the User collection.
var User = {
name: String,
//other fields
}
var Author = {
userId: {
type: String,
ref: "User"
}
}
Another solution is when updating name in User collection, update all names in Author collection.
I think first solution will be better.

How to access attributes of an embedded document (DBRef-like) from a Schema method with Mongoose?

I am building a nodejs app with express and mongoose/mongodb.
In order to manage user roles (a user may have 0-n roles), I decided to implement a User schema and a separate Role schema and tie them together like DBRefs and using mongoose populate capability to get from one to the other easily. I chose this structure because I thought this was the best way to answer things like: "Get me a list of users that have X role" or "Get me a list of users whose Y role is set to expire soon".
These are stripped down versions of my schemas for role and user:
RoleSchema = new Schema {
_user: { type: Schema.ObjectId, ref: 'User' }
type: String
expiration: { type: Date, default : '0' }
}
UserSchema = new Schema {
email: { type: String, index: { unique: true }}
roles : [{ type: Schema.ObjectId, ref: 'Role' }]
}
With this, I am able to create/fetch/populate users and roles from within my express scripts without a problem. When I want to add a role to a user, I create the role, save it and then push it to the roles array of the user and then save the user. So, what happens in mongoDB is that each schema has its own collection, and they point to each other through an id field.
Now, what I want to do next is implement boolean-type methods on my schema to check for role-related issues, so that I can do things like
if(user.isActiveSubscriber())
I thought that I would be able to accomplish this simply by adding a method to my User schema, like this:
UserSchema.method "isActiveSubscriber", ->
result = false
now = new Date()
#roles.forEach (role) ->
result = true if role.type is "SUBSCRIBER" and role.expiration > now
result
My problem is that roles are coming out with empty attributes. I suppose it makes sense, since my user collection only has the id of the role but the actual attributes are stored in another collection.
So, here go the questions:
a) Is there a way to load the roles attributes from within my user schema method?
I tried calling populate inside the method, but got an error that the user object doesnt know any populate method. I also tried doing a Role.findById() from inside the method but also get an error (tried with Role and RoleSchema)
b) In case there is not a way ... should I simply add the code to check in my scripts? I hate having to put this kind of logic mixed with application logic/flow. Is there a better option?
c) Was it a bad idea to separate these collections in two? Am I completely missing the point of NoSQL? Would it be better if roles were simply an array of embedded documents stored as part of the user collection?
Let me answer your questions:
a) What you can do is to load roles inside your call. Assuming you did
Role = mongoose.model('Role', RoleSchema)
you just need to run
Role.where('_id').in(user.roles).run( (err, res) ->
/* you have roles in res now */
)
this however is an asynchronous operation, which requires callback to be passed to your method, i.e.
UserSchema.method "isActiveSubscriber", (callback) ->
now = new Date()
if #roles.length is 0
return callback(false)
if #roles[0].type
/* roles are loaded,
maybe there is some other fancy way to do the check */
result = false
#roles.forEach (role) ->
if role.type is "SUBSCRIBER" and role.expiration > now
result = true
callback(result)
else
/* we have to load roles */
Role.where('_id').in(#roles).run( (err, res) =>
/* you have roles in res now */
/* you may want to populate the field,
note => sign in this function definition */
#roles = res
result = false
res.forEach (role) ->
if role.type is "SUBSCRIBER" and role.expiration > now
result = true
callback(result)
)
Now for a user user you can call it like this
user.isActiveSubscriber( (result) ->
/* do something with result */
)
The problem is that the operation is asynchronous and it forces additional callback nesting in your code (this will be pain if you want to check for roles for say 100 users, you will need some asynchronous calls handling library like async). So I advice populating this field whenever you load users and use the code you showed us. You can add static method (maybe you can even override default find method? I'm not sure about this though) for this.
b+c) The other option is to store roles as strings in your collection and hardcode possible roles in app. This will be simplier to implement, however adding/removing new role will be a pain in the $%^&*. The way you are doing this (i.e. via references) is fine and I think that the only thing you need is to populate the field whenever you search for users and everything will be fine.
The approach adopted is within the purview of how mongoose implements DBRef-like behavior using populate. However some things seem to have been missed out. Not sure if these details have already been take care and not shown for brevity. Will run through the complete set of steps:
RoleSchema = new Schema {
type: String
expiration: { type: Date, default : '0' }
}
UserSchema = new Schema {
email: { type: String, index: { unique: true }}
_roles : [{ type: Schema.ObjectId, ref: 'Role' }] // array of pointers for roles
}
Role = mongo.model('Role', 'RoleSchema');
User = mongo.model('User', 'UserSchema');
var life_sub = new Role({ type: 'LIFE_SUBSCRIBER', .... });
var daily_sub = new Role({ type: 'DAILY_SUBSCRIBER', .... });
var monthly_sub = new Role({ type: 'MONTHLY_SUBSCRIBER', .... });
var jane = new User({email: 'jane#ab.com', _roles: [daily_sub, monthly_sub]});
Add users as required. Next to find the specified user and the corresponding set of roles use:
User
.find({email:'jane#ab.com'})
.populate('_roles[]')
.run(function(err, user)
Now user is an array of pointers corresponding to the roles of the user specified. Iterate through the array and find each role for the user and the corresponding expiration date. Compare with Date.now() to arrive at which roles have expired for the particular user.
Hope this helps.

Resources