sailsjs where many to many relationship contains - javascript

Im trying to find all users that are doing a certain subject. Here is the relationship:
User.js
attributes: {
subjects: { collection: 'subject', via: 'users', dominant: true }, // Many to Many
levels: { collection: 'level', via: 'users', dominant: true } // Many to Many
}
Subject.js
attributes: {
users : { collection: 'user', via: 'subjects' } // Many to Many
}
Level.js
attributes: {
users : { collection: 'user', via: 'levels' } // Many to Many
}
I would like to be able to do a find on User and only return users that have a relationship to a specific subject. Something like this:
User.find({ subjects: { 'contains': 1 } })
.exec(function(err, results){
if(err) return res.serverError(err);
users = results;
return res.json(users);
});
I know that I can do it by doing:
Subject.findOne({id:1})
.populate('users')
.exec(function(err, results){
if(err) return res.serverError(err);
users = results.users;
return res.json(users);
});
However I do not want to do it this way as I may want to filter by more than just subject. For example using the above I may want to find all users that are doing subject 1 AND are doing level 2.
User.find({ subjects: { 'contains': 1 }, levels: { 'contains': 2 } })
.exec(function(err, results){
if(err) return res.serverError(err);
users = results;
return res.json(users);
});
I am using sails v0.12.3

There is no such mechanism as far as I know where you can search as you mentioned.
Though you can do it in following ways:
Using condition in populate
User.find()
.populate('subjects', {
name: 'subject-1'
})
.populate('levels', {
name: 'level-1'
})
.exec(function(err, user) {
if (err) {
// Error
}
sails.log.verbose(user);
});
Using async
async.auto({
subject: function(cb) {
Subject.findOne({
name: 'subject-1'
})
.exec(cb);
},
level: function(cb) {
Level.findOne({
name: 'level-1'
})
.exec(cb);
},
user: ['subject', 'level', function(cb, results) {
User.find({
subjects: results.subject.id,
levels: results.level.id
})
.exec(function(err, users) {
if (err) {
return cb(err);
}
sails.log.verbose(users); // Required user
});
}]
}, function(err, results) {
// Callback
});
Using .query() method where you have to write a JOIN query to fetch data.

Related

MongoDB/Mongoose $pull an Object in an Array where _id of the Object is matching

I have this Schema here
Consider the likedTours which is an Array of Objects (Tours) (ignore position 0).
I want to pull any Objects where the _id of a Tour matches the critiria.
Adding a new Tour upon liking a tour is okay, but on unlike I don't know how to pull that item out.
Here is my function in the Controller in the Node.JS backend
const unlikeTour = async (req, res) => {
try {
TourDB.Tour.findOneAndUpdate(
{ _id: req.params.tourid },
{
$pull: { likedUsers: req.userID },
$inc: { likes: -1 },
}
).exec(async (err, docs) => {
if (!err) {
try {
await UserDB.User.findOneAndUpdate(
{ _id: req.userID },
{ $pull: { 'likedTours._id': docs._id } } //Here I need help
).exec()
return res.status(200).send({ successMessage: 'Tour successfully unliked' })
} catch (err) {
return res.status(500).send({ errorMessage: 'User not found' })
}
} else {
return res.status(500).send({ errorMessage: 'Tour not found' })
}
})
} catch (err) {
return res.status(500).send({ errorMessage: err })
}
}
This method looks for a tour and update it by pulling out the userID and decrement the likes count by -1.
And then I try to find in the UserDB that tour in the likedTours and tried to pull but it doesn't not work like that.
Thanks in advance
you can update as
await UserDB.User.findOneAndUpdate(
{ _id: req.userID },
{ $pull: { likedTours: { _id: docs._id } } } //Here I need help
).exec();
reference: https://docs.mongodb.com/manual/reference/operator/update/pull/

Trying to increment by 1 every time the page is viewed

I'm trying to figure out how to update the field by incrementing +1 each time the page is visited and if it has never been visited then add it to the DB.
Currently, this is what I have got but it does not seem to do much. I must have gone wrong somewhere and I have not yet implemented the part where if the page has never been viewed then create a new object in the array which is stored in the database.
Little note: Where I created the map they do match with the same ID if I view the page with the same ID as the one stored in the database but no increment happens.
exports.pageVisitCount = (req, res, next) => {
User.findById({
_id: req.userData.userId
}, 'visits', function (err, pageVists) {
if (err) {
res.status(401).json({
message: "Error Occured!"
})
} else {
const pageCounts = pageVists.visits;
pageCounts.map(page => {
const postViewed = req.body.postId;
if (page.postId.toString() === postViewed) {
User.findByIdAndUpdate({
_id: req.userData.userId
}, {
$set: {
visits: [{
"postId": postViewed,
$inc: { visitCount: 1 }
}]
}
}, {
upsert: false
},
(err) => {
if (err) {
res.status(401).json({
message: "Error Occured!"
})
} else {
res.status(200).json({
message: "Update successful!"
})
}
});
}
});
}
});
}
This is the schema I am using:
const visitsSchema = new Schema ({
postId: {
type: String
},
visitCount: {
type: Number
}
})
const userSchema = mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
role: {
type: String,
required: true
},
answers: {
type: String
},
visits: [visitsSchema]
});
Any feedback would be highly appreciated, I would like to mention that I am new to backend, thanks!
To avoid using the map to filter the visits after querying the visits of the user under consideration, I suggest you let mongodb do that for you. In this case you first do a find based on both the user id and the postId. If you get a record matching both criteria you are sure you can easily update the user visits by incrementing the particular visits visitCount by 1.
Otherwise i.e. if they don't match any records then since u might be using a valid user id then such user has not visited such post. So you now create a new visit with the postId and initialize its visitCount to 1 (Although we intend to create, but since its a subdocument you'll need use $push). Enough of the talking try the code below.
exports.pageVisitCount = (req, res, next) => {
User.findOne({
_id: req.userData.userId, "visits.postId": req.body.postId
}, 'visits.$', function (err, user) {
if (err) {
res.status(401).json({
message: "Error Occured!"
});
} else {
if(user == null){
User.findByIdAndUpdate({
_id: req.userData.userId
}, {
$push: {
visits: {
"postId": req.body.postId,
visitCount: 1
}
}
}, function (err) {
if(err)
return res.status(401).json({
message: "Error Occured when creating new visit!"
})
return res.status(200).json({
message: "Success"
})
})
}
User.update({
_id: req.userData.userId, "visits.postId": req.body.postId
}, {
$inc: { "visits.$.visitCount": 1 }
},(err) => {
if (err) {
res.status(401).json({
message: "Error Occured!"
})
} else {
res.status(200).json({
message: "Update successful!"
})
}
});
}
});
};

How could I reference a model to my User model with Express/Mongoose

I have two models, one being my User model and the other being my Course model. I would like to have it so when a User (Teacher) creates a course, it assigns that course to them and vice versa. Here are my models to explain better:
Course Schema/Model:
var CourseSchema = new Schema({
courseID: {
type: Number,
unique: true
},
courseName: String,
courseDesc: {
type: String,
default: "No course description provided."
},
coursePicture: {
type: String,
required: false
},
teacher: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
],
students: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Student'
}
]
})
User Schema/Model:
var UserSchema = new mongoose.Schema({
firstName: String,
lastName: String,
email: String,
courses: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Course'
}
],
password: String
});
Basically, I want to have it so on my frontend, I could do things like course.teacher.firstName or user.courses. My schemas are in two different files, but I believe that is fine. It's like assigning a user a post when they create it. I don't know how I could do this, as I've tried multiple things.
Right now, I currently have this for creating a course.
// Creates a new course
router.post('/create', function (req, res) {
Course.create({
courseID : req.body.courseID,
courseName : req.body.courseName,
courseDesc : req.body.courseDesc,
coursePicture : req.body.coursePicture,
teacher : req.body.id,
students: req.body.students
},
function (err, course) {
if (err) return res.status(500).send("There was a problem adding the information to the database.");
res.status(200).send(course);
});
});
I have already referenced the User model in the controller where that code ^ belongs as so var User = require('../user/User');
I believe that is needed to pull this off. If you have any questions, please let me know as I'm not the best at explaining things like this.
Hope someone can help me out!
Thanks.
// Creates a new course
router.post('/create', function (req, res) {
Course.create({
courseID : req.body.courseID,
courseName : req.body.courseName,
courseDesc : req.body.courseDesc,
coursePicture : req.body.coursePicture,
teacher : req.body.id, // find this user
students: req.body.students,
attendance: req.body.attendance
},
function (err, course) {
User.findById(req.body.id, function(err, user) {
user.update({
$push: {
courses: course._id
}
}, function(err) {
if (err) return res.status(500).send("There was a problem adding the information to the database.");
res.status(200).send(course);
})
})
});
});
This is an issue of database design. There should only be one place where information about a course is stored, the Courses table, and the Users table should know nothing about courses. There should be a table the relates a course to a user: a UserCourseRelations table.
I would strongly avoid the approach of storing an array of courseIds that a user is related in the user table as this is unnecessary coupling and so is not good database design. Also, it'll bog down reads to your Users table as those arrays grow on every row.
Here's how I would approach this. Note that some of this code uses ES6 syntax. The following code is untested, but should work. Take a look:
Create CourseSchema and CourseModel
var CourseSchema = new mongoose.Schema({
courseID: {
type: Number,
unique: true
},
courseName: String,
courseDesc: {
type: String,
default: "No course description provided."
},
teacherId: {
type: mongoose.Schema.Types.ObjectId,
}
coursePicture: {
type: String,
required: false
},
students: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Student'
}
]
})
CourseSchema.statics.createNew = function(data, callback) {
// do some verification here
// insert the new course
return new this(data).save((err, dbCourse) => {
if (err) {
return callback(err)
}
UserCourseRelationSchema.insertNew('teacher', userId, courseID, (err, dbUserCourseRelation) => {
if (err) {
return callback(err)
}
// done. return the new course
callback(null, dbCourse)
})
})
CourseSchema.statics.getByIds = function(courseIDs, callback) {
// find all of the courses where the courseID is in the courseIDs array
// see https://docs.mongodb.com/manual/reference/operator/query/in/
this.find({courseID: {$in: courseIDs}}, (err, courses) => {
if (err) {
// something went wrong
return callback(err)
}
callback(null, courses)
})
}
}
let CourseModel mongoose.model('courses', CourseSchema);
Create UserCourseRelationSchema and UserCourseRelationModel that relates a course to a user and vice versa
var UserCourseRelationSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
},
courseID: {
type: Number,
required: true,
},
type: {
type: String,
enum: ['teacher', 'student'],
required: true,
},
});
UserCourseRelationSchema.statics.createNew = function(type, courseID, userId, callback) {
// do some verification here. I suggest making sure this relation doesn't already exist
// insert the new course
return new this({
courseID: courseID,
userId: userId,
type: type,
}).save((err, dbUserCourseRelation) => {
if (err) {
return callback(err)
}
// return the new relation
callback(null, dbRelation)
})
}
UserCourseRelationSchema.statics.getTeacherRelationCourseIdsByUserId = function(userId, callback) {
let query = this.find({userId: userId, type: 'teacher'})
query.distinct('courseID') // get an array of only the distinct courseIDs
query.exec((err, courseIDs) => {
if (err) {
// something went wrong
return callback(err)
}
callback(null, courseIDs)
})
}
let UserCourseRelationModel = mongoose.model('user_course_relations', UserCourseRelationSchema);
Create UserSchema and UserModel
var UserSchema = new mongoose.Schema({
firstName: String,
lastName: String,
email: String,
password: String
});
UserSchema.statics.getAllCoursesById = function(userId, callback) {
// get the relations for the courses the user is a teacher of
UserCourseRelationModel.getTeacherRelationCourseIdsByUserId(userId, (err, courseIDs) => {
// get the courses by the returned coursIDs
CourseModel.getByIds(courseIDs, (err, courses) => {
if (err) {
// something went wrong
return callback(err)
}
callback(nul, courses)
})
})
}
let UserModel = mongoose.model('users', UserSchema);
// -- create the router
// Creates a new course
router.post('/create', function (req, res) {
CourseModel.createNew({
courseID : req.body.courseID,
courseName : req.body.courseName,
courseDesc : req.body.courseDesc,
coursePicture : req.body.coursePicture,
teacher : req.body.id,
students: req.body.students
}, function (err, course) {
if (err) return res.status(500).send("There was a problem adding the information to the database.");
res.status(200).send(course);
});
});
// -- done
I also suggest using promises if possible as it makes all of this logic much simpler.

How to handle results of multiple async callbacks in Node

I'm working on a scenario in Node which involves multiple async.map calls, one of which is nested in an _.map function as I need access to nested arrays. I'm struggling to properly combine the results of these calls into an array which I return in the end.
More specifics on the scenario:
I start with a userId and query my games db to find all games which this userId is associated with. I pull gameId and description for each game.
I then query the gameId of each of these games to find all userIds associated with each game. I filter the original userId out of these results.
Next I query my users db for each of these userIds in order to get details such as user email and name. (Note: this is where _.map comes into play as the userIds are nested in an array of arrays; each inner array representing a game).
The last step, and where I'm struggling, is how to combine these results and return them. My final desired array looks like the below. An array of game objects. Each game object has a gameId, description, and users property. users is an array of users associated with that game and has id, email, and name properties.
[
{
gameId: 'dfh48643hdgf',
description: 'lorem ipsum...',
users: [
{
id: 1,
email: 'test#example.com',
name: 'John Doe'
},
{
id: 7,
email: 'sample#example.com',
name: 'Jane Smith'
}, ...
]
}, ...
]
Below is the code I currently have (Note: this code block is actually part of a larger async.series call):
sharedGames: function(next) {
db.games.index({userId: userId},
function(err, gamesData) {
if (err) {
logger.warn('Error retrieving games for userId %d', userId, err);
next(err, null);
} else {
gamesData = _.map(gamesData, function(game) {
return {
gameId: game.id,
description: game.description,
users: []
};
});
async.map(gamesdata,
function(item, callback) {
db.gameDetails.getGameUsers({gameId: item.gameId},
function(err, users) {
if (err) {
callback(err, null);
} else {
callback(null, _.without(users.userIds, Number(userId)));
}
}
);
},
function(err, results) {
if (err) {
next(null, null);
} else {
var flattenedResults = _.chain(results)
.flatten()
.uniq()
.value();
async.map(flattenedResults, function(user, callback) {
db.users.getById(user, function(err, userDetails) {
if (err) {
callback(err, null);
} else {
callback(null, _.pick(userDetails, 'id', 'email', 'name'));
}
});
}, function(err, users) {
if (err) {
next(null, null);
} else {
_.each(results, function(result, index) {
_.each(result, function(user) {
var customerDetails = _.find(customers, function(u) {
return u.id === user;
});
gamesData[index].users.push(userDetails);
});
});
console.log(gamesData);
next(null, gamesdata);
}
});
}
}
);
next(null, []);
}
}
);
}
So you can definitely accomplish what you're describing with async.auto. Check this from their docs:
async.auto({
get_data: function(callback){
console.log('in get_data');
// async code to get some data
callback(null, 'data', 'converted to array');
},
make_folder: function(callback){
console.log('in make_folder');
// async code to create a directory to store a file in
// this is run at the same time as getting the data
callback(null, 'folder');
},
write_file: ['get_data', 'make_folder', function(callback, results){
console.log('in write_file', JSON.stringify(results));
// once there is some data and the directory exists,
// write the data to a file in the directory
callback(null, 'filename');
}],
email_link: ['write_file', function(callback, results){
console.log('in email_link', JSON.stringify(results));
// once the file is written let's email a link to it...
// results.write_file contains the filename returned by write_file.
callback(null, {'file':results.write_file, 'email':'user#example.com'});
}]
}, function(err, results) {
console.log('err = ', err);
console.log('results = ', results);
});
So if you follow the same approach, you can get the array of game objects by injecting more info every call. You can also use async.waterfall. Hope it helps

Sails.js: Inserting one-to-many associated models to MongoDB

I am trying to take advantage of the Waterline ORM in Sails.js to build an example app that has a model called 'Category'. Because a category can have multiple sub categories, I have the following one-to-many association for this model:
module.exports = {
adapter: 'mongo',
// adapter: 'someMysqlServer',
attributes: {
categoryTitle: {
type: 'string',
required: true
},
parentCat: {
model: 'category'
},
subCategories: {
collection: 'category',
via: 'parentCat'
},
articles: {
collection: 'article',
via: 'category',
required: false
}
}
};
In the CategoryController.js, I have the create method that first tries to see if the new category has a parent category assigned to it; however, I feel the code is quite messy, and the parentCat in Mongodb is always empty even if I tried to assign a parent category in the form submission. So I am wondering if this is the right way to do it:
create: function(req, res, next) {
var params = req.allParams();
// set parent category if exists
if (params.parentCat) {
Category.findOne({categoryTitle : params.parentCat})
.exec(function(err, category) {
if (err) {
return false; //not found
} else {
params.parentCat = category.id; //found the parent category
console.log('parent cat id is: ', category.id);
}
});
}
Category.create(params, function(err, newCategory) {
if (err) {
return next(err);
} else {
console.log('new category created');
}
console.log('successfully added the category: ' + newCategory.categoryTitle)
res.redirect('/category');
}); // create the category
}
The issue of your code is the callback.
I created a new version of code with the async feature (which is already in your sails app), hope it will help you.
create: function(req, res, next) {
var params = req.allParams();
async.waterfall([
function(callback) {
// set parent category if exists
if (params.parentCat) {
Category.findOne({
categoryTitle: params.parentCat
})
.exec(function(err, category) {
if (err) {
return false; //not found
}
params.parentCat = category.id; //found the parent category
console.log('parent cat id is: ', category.id);
callback(null, params);
});
} else {
callback(null, params);
}
},
function(params, callback) {
Category.create(params, function(err, newCategory) {
if (err) {
return next(err);
}
console.log('successfully added the category: ' + newCategory.categoryTitle);
callback(null, newCategory);
}); // create the category
}
], function(err, result) {
console.dir(result);
res.redirect('/category');
});
}

Categories