Mongoose: Modeling and updating many to many - node.js

I'm very new to mongodb and mongoose and I'd like to know how to handle many to many relations. I know mongodb is a non-relational database, but how would I model something like Users and Groups.
A User can join multiple groups while a group can have multiple users.
My Schemas look like this:
User
const UserSchema: Schema = new Schema({
username: {
type: String,
required: true,
unique: true,
},
groups: [
{
type: Schema.Types.ObjectId,
ref: 'Group',
},
],
});
Group
const GroupSchema: Schema = new Schema({
name: {
type: String,
required: true
},
users: [
{
type: Schema.Types.ObjectId,
ref: 'User',
},
],
});
So, with an endpoint like 'users/:id/assigngroup/:groupid I would like to add an User_id to User.groups and the groupid to Group.users.
For my endpoint I do something like this:
User.findOneAndUpdate({ _id }, { $addToSet: { groups: groupId } }, { new: true }).populate('groups').exec((err, user) => {
Group.findOneAndUpdate({ _id: groupId }, { $addToSet: { users: _id } }, { new: true }).exec((err, group) => {
res.send(user);
});
})
This works tho, but the problems I encountered are:
I could still add a groupId to User.users even if the Group with this id doesn't exist. I could check this with an additional Group.findOne() before the User.findOneAndUpate but this doesn't feel like it's the best/quickest way in mongoose.
How should I handle a rollback if, for example one of the findOneAndUpdate, fails?
What I need is a clean way to check if the Group with the groupid exists, then check if the user with the _id exists and after that I can add the groupid to User.groups and the _id to Group.users (and not being able to add the same id multiple times of course). What would be the best way to do this? Using findOne/findOneUpdate mutiple times for each check doesn't feel like the right way.
Please let me know if my approach to model this "relation" is completely wrong.
Thanks! :)
Edit: I don't think it's the smart way to save a reference in both models. I removed groups from the User model. So to assign a User I just use
Group.findOneAndUpdate({ _id: groupId }, { $addToSet: { users: _id } }, { new: true }).populate('users', '_id username').exec().then(group => { });
And the easy way to get all Groups of a User would be
Group.find({
"users": new Types.ObjectId(id)
}).exec(callback);
So I don't need to update a reference twice if I assign/remove a User.

Related

Querying objects for an array of attributes in Mongoose

I am trying to fetch chats for an array of users.
Chats schemas are defined like this:
const ChatSchema = new Schema<IChatSchema>(
{
messages: [
{
type: Schema.Types.ObjectId,
ref: "MessageSchema",
},
],
participants: [
{
type:Schema.Types.ObjectId,
ref: "UserSchema",
}
]
},
{
timestamps: true,
}
);
I have two usersnames 'A' and 'B' and I want to query common chats of those two users. Any idea how to do it?
User schema
const UserSchema = new Schema<IUserSchema>(
{
username: {
type: String,
required: true,
unique:true,
},
},
{
timestamps: true,
}
);
I tried this approach but did not work.
let chat = await Chats.find({
participants: { $elemMatch: { username: usernames } },
})
I also tried this
let chat = await Chats.find({
"participants.username": { $all: usernames },
})
I can think of a few ways:
Option A: Use aggregation to lookup the participants, then match the usernames
Option B: Use find to retrieve the user records from Users, then query Chats for matching ObjectID values
Option C: modify the schema so the chats also contain the usernames, so you can query them directly

How to update a document using update query in mongoose?

This is the comment key pair I have in my post model:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const postSchema = new Schema({
user:{
type:Schema.Types.ObjectId,
// required:true,
refPath:'onModel'
},
onModel:{
type:String,
enum:['Doctor','Patient']
},
text:{
type:String,
required:true
},
comments:[{
user:{
type:Schema.Types.ObjectId,
refPath:'onModel'
},
reply:{
type:String
},
date:{
type:Date,
default:Date.now
}
}],
likes:[{
user: {
type: Schema.Types.ObjectId,
ref: 'Patient'
}
}]
})
module.exports= post = mongoose.model('post', postSchema);
When I try pushing object to the likes array by running the following code, it fails. The filter part works fine, just some problem with the update part which ends up executing catch block.
Post.updateOne({ _id: req.params.postid, "likes": { $ne : { user:
authorizedData.jwt_payload.patient._id }}},
{ "$set" : { "likes.$.user": "authorizedData.jwt_payload.patient._id"
}})
.then(re => res.json(re))
.catch(err => res.json("already liked"))
Will really appreciate any help.
Please make changes as below :
const mongoose = require('mongoose');
const patientObjectID = mongoose.Types.ObjectId(authorizedData.jwt_payload.patient._id);
Post.updateOne({
_id: req.params.postid,
'likes.user': {
$ne:
patientObjectID
}
},
{ $push: { likes: { user: patientObjectID } }
}).then(re => res.json(re)).catch(err => res.json("already liked"))
Couple of changes need to be done, So When you've a schema like this ::
likes: [{
user: {
type: Schema.Types.ObjectId,
ref: 'Patient'
}
}]
You need to pass an ObjectId() to user field but not a string, So
first we're converting string to ObjectId() & passing it in query.
Also $set is used to
update existing or insert new fields in a document, but when you wanted to push
new values to an array field in a document then you need to use
$push(this seems to be a normal update operation on a field, but here we're not replacing the likes array, rather we're just adding few more elements to it - different kind of update though, So that's why we need to use $push).
As you already have below filter, we're just doing $push assuming what we're pushing is not a duplicate but in the other way you can blindly use $addToSet to do the same without having to use below filter criteria :
"likes": {
$ne: {
user:
patientObjectID
}
}
About your question on $(update) why it isn't working ? This should be used to update the elements in an array, it helps to update first matching element in an array based on filter criteria, but what you wanted to do here is to add few more elements but not updating existing elements in likes array.
Here you should not send "already liked" in catch block, it should be a custom error for an actual error, in .then(re => res.json(re)) you need to check write result of update operation if anything updated you need to send added user, if not you need to send "already liked".
Hope this solves all your questions :-)
Try using $push aggregation which is used for pushing objects to inner arrays in mongoDB. Your update query should be something like the following:
Post.updateOne({ _id: req.params.postid, "likes": { $ne : { user:
authorizedData.jwt_payload.patient._id }}},
{ "$push" : { "likes": authorizedData.jwt_payload.patient._id
}})
.then(re => res.json(re))
.catch(err => res.json("already liked"))

Mongoose middleware when adding multiple elements to a mongo array

Basically I have two mongodb collections, users and projects.
I want to keep track on which projects a user is working on,
and also which users that are working on a given problem.
const User = mongoose.model('User', new Schema({
name: {
type: String,
},
_projects: [{
type: Schema.ObjectId,
ref: 'Project'
}]
})
and
const Project = mongoose.model('Project', new Schema({
title: {
type: String,
},
_usersWorkingOn: [{
type: Schema.ObjectId,
ref: 'User',
}]
})
Now, let's say i want to add multiple existing user-id to an existing project.
What would best practice be?
Right now I'm using findOneAndUpdate to get the project document to push the user-id to the array. And then a mongoose middleware were I take the last added entry and get the user-id from there:
Schema.post('findOneAndUpdate', addProjectToUser)
function addProjectToUser(updatedProject, next) {
User.findByIdAndUpdate(
updatedProject._usersWorkingOn[updatedProject._usersWorkingOn.length - 1],
{ $push: { '_projects': updatedProject._id}},
{ 'new': true }
).then(() => {
next()
})
}
This works when I add one user to a project. But if a want to add multiple users in one save, addProjectToUser have no way of knowing how many entries were added to the array.
Am I missing something here, or thinking about it all wrong?

add a item inside a nested schema mongoose with addToSet

I know populating schemas is not a new question but I am having a little trouble following the logic on this in regards to multiple schemas. I am working with
"mongoose": "^4.8.5",
"express": "^4.15.0",
I have a schema with a collection of caffeine drinks. When a user selects a drink i would like for that drink to be assigned to the user.
** If at any point I am missing something simple in the architecture please let me know. This project has been my intro to mongodb.
I am reading through populating on the mongoose documentation http://mongoosejs.com/docs/populate.html.
Essentially, if I am to assign the drinks to the list it looks like I want to add them as a reference in an array. This was my approach with caffeine_list
const SelectedDrinks = require('./userDrinks');
const UserSchema = mongoose.Schema({
name: {
type: String
},
email: {
type: String,
required: true
},
username: {
type: String,
required: true
},
password: {
type: String,
required: true
},
caffeine_list: caffeine_list: [ // attempting to reference selected drinks
{
type: mongoose.Schema.Types.ObjectId,
ref: 'SelectedDrinks'
}
]
})
SelectedDrinks comes from the schema below. I added a reference to the user as the creator below
const User = require('./user');
let userDrinkSchema = new mongoose.Schema({
creator : {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
caffeine: Number,
mgFloz: Number,
name: String,
size: Number,
updated_at: {
type: Date,
default: Date.now()
}
});
This is where I start to get confused. I initially tried populate but could not get it going. If that was correct please let me know.
In regards to my task of adding a selected drink to the user I used addToSet. I was hoping that this would give me the drink info. I did my set up like so....
const User = require('../../models/user');
const UserDrinks = require('../../models/userDrinks');
router.post('/addDrink', (req, res, next) => {
let newDrink = new UserDrinks({
creator: req.body.creator,
caffeine: req.body.caffeine,
mgFloz: req.body.mgFloz,
name: req.body.name,
size: req.body.size,
updated_at: req.body.updated_at
});
newDrink.save( (err) => {
if(err) {
res.send(err);
} else {
User.findOne({ _id: newDrink.creator}, (err, user) => {
user.caffeine_list.addToSet(newDrink)
user.save( function (err) {
if(err) {
console.log(err);
}else {
res.status(201).json(newDrink);
}
})
})
}
})
});
However, after i do a post in postman I check caffeine_list and the result is
"caffeine_list" : [
ObjectId("58d82a5ff2f85e3f21822ab5"),
ObjectId("58d82c15bfdaf03f853f3864")
],
Ideally I would like to have an array of objects being passed with the caffeine info like so
"caffeine_list" : [
{
"creator": "58d6245cc02b0a0e6db8d257",
"caffeine": 412,
"mgFloz": 218.7,
"name": "1.95 Perfect Drink!",
"size": 42.93,
"updated_at": "2017-03-24T18:04:06.357Z"
}
]
Change your else part with below code instead of findOne and save use update
User.update(
{ _id: newDrink.creator},
{ $addToSet:{
caffeine_list: newDrink
}}).exec(function (err, updatedrink){
if(err) {
console.log(err);
}else {
res.status(201).json(updatedrink);
}
})
Although I am not sure this is the best approach I did find this to be give me the result that I was desiring. I had to make two small changes and I was able to get the caffeine_list to give me the desired response
I had to access the schema for selected drinks
const SelectedDrinks = require('./userDrinks').schema; //** need schema
Afterwards I was able to change
caffeine_list: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'UserDrinks' // name of the file
}
]
to
caffeine_list: [SelectedDrinks]
Now that I have the schema I am able to add the drinks directly into the caffeine_list on the UserSchema.

Mongoose: how to structure a schema with a SET of subdocuments (one unique field)?

I'm trying to make the following schema to work:
var FormSchema = new mongoose.Schema({
form_code: { type: String, unique: true },
...
});
var UserSchema = new mongoose.Schema({
...
submissions: [{
form_code: { type: String, unique: true },
last_update: Date,
questions: [{
question_code: String,
answers: [Number]
}]
}],
});
The rationale here is that a user can have many unique forms submitted, but only the last submission of each unique form should be saved. So, ideally, by pushing a submission subdocument when updating a user, the schema would either add the submission object to the set, or update the subdocument containing that form_code.
The following code doesn't work as desired (it pushes the new subdocument even if the form_code is already present):
User.findOneAndUpdate(
{ _id: user.id },
{ $addToSet: { submissions: submission_object } },
function (err, user) {
// will eventually have duplicates of form_code at user.submissions
}
);
The above schema clearly doesn't work, what must be changed to achieve that "upsertToSet"?

Resources