User with favourites and likes system in Mongoose schemas - node.js

I want to create a DB with Users which also have a reference to another DB called "Library" which has "favourites" and "likes". I will show the idea here:
User Model
const userSchema = Schema({
username: {type: String, minlength: 4, maxlength: 10, required: true, unique: true},
email: {type: String, required: true, unique: true},
password: {type: String, required: true},
isVerified: { type: Boolean, default: false },
library: {type: Schema.Types.ObjectId, ref: 'Library'}
}, { timestamps: true});
Library Model
const librarySchema = new Schema({
likes: [{
likeId: {type: String},
mediaType: {type: String}
}],
favourites: [{
favId: {type: String},
mediaType: {type: String}
}],
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
Can you please tell me if this is the right way to implement these models or if there is a better way?
At the moment if I try to call
User.findOne({email: 'xxx#xxx.com'}).populate('library').exec(function (err, library)
it doesn't find anything...
Library POST request
router.post('/favourites', passport.authenticate('jwt', {session: false}), function (req, res) {
const favouritesFields = {};
if (req.body.favId) favouritesFields.favId = req.body.favId;
if (req.body.mediaType) favouritesFields.mediaType = req.body.mediaType;
Library.findOne({user: req.user._id}).then(library => {
if (library) {
Library.update({user: req.user._id}, {$push: {favourites: favouritesFields}})
.then(library => res.json(library));
} else {
new Library({user: req.user._id, favourites: favouritesFields}).save().then(library => res.json(library));
}
});
});
User POST request
router.post('/signup', function (req, res) {
const {errors, isValid} = validateSignupInput(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
// Check if email already exists
User.findOne({email: req.body.email}, function (user) {
if (user) {
return res.status(400).json({
title: 'Email already exists'
});
}
});
// Create and save the new user
let user = new User({
username: req.body.username.toLowerCase(),
email: req.body.email.toLowerCase(),
password: bcrypt.hashSync(req.body.password, 10)
});
user.save(function (err, result) {
if (err) {
return res.status(500).json({
title: 'An error occurred during the signup',
error: err
});
}
res.status(201).json({
title: 'User created',
obj: result
});

Your problem is not with the query you're making. there is no foundUser.library because one was never added.
You're adding users to libraries, but you're not adding libraries to your users. if you run the following code in your app:
Library.find({}).populate("user").exec(function(err, foundLibraries){
if (err){
console.log(err);
} else {
console.log(foundLibraries);
}
});
You would see that the libraries have their "user" properties, that when populated contain the entire user document as an object. But, the reason that isn't working for foundUser.library when you query for users is that foundUser.library was never assigned. you know how you're assigning the email, username and password when creating users, you have to do the same for the library property. Or, in your case, since a library is only created after the user, you can just set the value of user.library in the callback of creating/saving the library.

Related

Log a user in and get their profile

I am attempting to log a user in to my DB. When I log the user in, it returns the first userId in the DB and not the user who logged in. I have been struggling with this for a while and really am at a dead end.
This is my POST route to log the user in:
// login
router.post("/login", async (req, res) => {
const user = await User.findOne({
email: req.body.email,
});
const secret = process.env.SECRET;
if (!user) {
return res.status(400).send("the user not found!");
}
if (user && bcrypt.compareSync(req.body.password, user.passwordHash)) {
const token = jwt.sign(
{
userId: user.id,
isAdmin: user.isAdmin,
},
secret,
{ expiresIn: "1d" }
);
res.status(200).send({ user: user.email, token: token });
} else {
res.status(400).send("password is wrong!");
}
});
The const user = await User.findOne({ email: req.body.email, }); this returns the wrong user.
When I query the endpoint get a users profile with the userId it gets the right information. So its got nothing to do with the DB.
This is the call in the app.
const handleSubmit = () => {
axios
.post(`${baseURL}users/login`, {
email: email,
passwordHash: password,
})
.then(res => {
console.log('USER ID TOKEN', res.data.token);
setbearerToken(res.data.token);
AsyncStorage.setItem('bearerToken', res.data.token);
const decoded = decode(res.data.token);
setTokenID(decoded.userId);
dispatch(setUser(res.data));
});
};
user.js model
const userSchema = mongoose.Schema({
contactName: {
type: String,
required: true,
minlength: 5,
maxlength: 50
},
phone: {
type: String,
required: true,
minlength: 5,
maxlength: 50
},
passwordHash: {
type: String,
required: true,
minlength: 5,
maxlength: 1024
},
token: {
type: String,
},
isAdmin: {
type: Boolean,
default: false
},
clubName: {
type: String,
required: true,
},
clubAddress: {
type: String,
required: true,
},
clubEmail: {
type: String,
required: true,
},
clubPhone: {
type: String,
required: true,
},
clubWebsite: {
type: String,
required: true,
},
clubContact: {
type: String,
required: true,
},
})
Your schema doesn't have a field email to filter on.
const user = await User.findOne({
email: req.body.email,
});
Maybe you try clubEmail field. I reproduced the behavior and it looks like that mongoose ignores the filter if the field does not exist in the Schema an just returns the first document in the collection.
E.g.
const userSchema = new Schema(
{
name: String,
age: Number
}
)
const User = mongoose.model('User', userSchema);
User.findOne({name: "Superman"}, ...
Returns the user with name "Superman".
const userSchema = new Schema(
{
name: String,
age: Number
}
)
const User = mongoose.model('User', userSchema);
User.findOne({xname: "Superman"}, ...
But when using xname in the filter document which does not exist in my schema neither in the collection as field the query returns the first document in my test collection (its not Superman).
Also look here similar issue: Model.find Mongoose 6.012 always return all documents even though having filter
Issue reported: https://github.com/Automattic/mongoose/issues/10763
Migration Guide to Mongoose 6:
https://mongoosejs.com/docs/migrating_to_6.html#strictquery-is-removed-and-replaced-by-strict

MongoDB/Mongoose Populate

Working with Mongoose "Populate" - So far I'm unable to successfully get the "Food" model to populate the "User" model.
The goal is to be able to save a "Food" to a user.
USER MODEL:
var UserSchema = new mongoose.Schema({
username: String,
password: String,
foods: [{ type: mongoose.Schema.Types.ObjectId}],
easy: {type: Boolean, default: false},
});
UserSchema.plugin(passportLocalMongoose)
module.exports = mongoose.model("User", UserSchema);
FOOD MODEL:
var foodSchema = new mongoose.Schema({
name: { type: String, required: false, unique: true },
author: {
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
}
});
module.exports = mongoose.model("Food", foodSchema);
GET ROUTE
router.get("/dashboard", function (req, res) {
User.find({currentUser: req.user})
.populate({path: 'foods'}).
exec(function (err, foods) {
if (err) return (err);
console.log('The food is:', req.user.foods.name);
});
});
POST ROUTE:
router.post("/dashboard", function(req, res, next) {
User.update({ id: req.session.passport.user }, {
}, function(err, user) {
if (err) return next(err);
User.findById(req.user._id, function(err, user) {
var newFood = new Food({
name: req.body.currentBreakfast,
image: 'test',
});
user.foods = newFood
user.save();
});
});
res.redirect('/dashboard');
});
You need to add the ref field in your user schema for foods to be populated while querying user.
var UserSchema = new mongoose.Schema({
username: String,
password: String,
foods: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Food' }],
easy: {type: Boolean, default: false},
});
You can user this query.
await User.find({currentUser: req.user}).populate('foods')
Try this it will auto-populate data
var UserSchema = new mongoose.Schema({
username: String,
password: String,
foods: [{ type: mongoose.Schema.Types.ObjectId,ref: 'Food'}}],
easy: {type: Boolean, default: false},
});
UserSchema.pre('find', prepopulate)
function prepopulate(){
return this.populate('foods')
}

ERROR: ValidationError: CastError: Cast to ObjectID failed for value

SITUATION:
It seems I must have made a mistake in my Mongoose Model or in one of the parameters that are passed to the route.
I am fairly new to the angular2 architecture, so the mistake might be quite obvious.
ERROR:
ERROR: ValidationError: CastError: Cast to ObjectID failed for value "{ title: 'das',
username: 'John',
choice1: 'FSDAFASDF',
choice2: 'FDSAFD',
counter1: 11,
counter2: 0,
pollId: '5920598ade7567001170c810',
userId: '591c15b3ebbd170aa07cd476' }" at path "poll"
CODE:
route
router.patch('/', function (req, res, next) {
var decoded = jwt.decode(req.query.token);
User.findById(decoded.user._id, function (err, user) {
user.votes = req.body.votes;
user.save(function(err, result) {
if (err) {
console.log("ERROR: "+err);
return res.status(500).json({
title: 'An error occurred',
error: err
});
}
res.status(201).json({
poll: 'Vote Saved',
obj: result
});
});
});
});
models/user:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var mongooseUniqueValidator = require('mongoose-unique-validator');
var schema = new Schema({
firstName: {type: String, required: true},
lastName: {type: String, required: true},
password: {type: String, required: true},
email: {type: String, required: true, unique: true},
polls: [{type: Schema.Types.ObjectId, ref: 'Poll'}],
votes: [{
poll: {type: Schema.Types.ObjectId, ref: 'Poll'},
choice: {type: Number},
}],
});
schema.plugin(mongooseUniqueValidator);
module.exports = mongoose.model('User', schema);
models/poll
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var User = require('./user');
var schema = new Schema({
title: {type: String, required: true},
choice1: {type: String, required: true},
choice2: {type: String, required: true},
counter1: {type: Number, required: true},
counter2: {type: Number, required: true},
user: {type: Schema.Types.ObjectId, ref: 'User'}
});
schema.post('remove', function (poll) {
User.findById(poll.user, function (err, user) {
user.polls.pull(poll);
user.save();
});
});
module.exports = mongoose.model('Poll', schema);
EDIT:
router.patch('/', function (req, res, next) {
var decoded = jwt.decode(req.query.token);
console.log("VALID ID ? :"+mongoose.Types.ObjectId.isValid(decoded.user._id));
console.log("DECODED USER ID:"+ decoded.user._id);
User.findByIdAndUpdate(decoded.user._id, {votes: req.body.votes}, function (err, user) {
user.save(function(err, result) {
if (err) {
console.log("ERROR: "+err);
return res.status(500).json({
title: 'An error occurred',
error: err
});
}
res.status(201).json({
poll: 'Vote Saved',
obj: result
});
});
});
});
I'm thoughtfully guessing that this particular piece of code is what causes the issue:
...
User.findById(decoded.user._id, function (err, user) {
user.votes = req.body.votes;
user.save(function(err, result) {
...
mongoose is trying to resave the model and overwrite it's _id property with a plain string, whereas it should be an instance of the ObjectId.
Instead of using save to update your model, please try to use findByIdAndUpdate instead. If this is working, than my guess would be correct.
User.findByIdAndUpdate(decode.user._id, {votes: req.body.votes}, function (err, user) {
Or, cast the string _id into an ObjectId manually
...
User.findById(decoded.user._id, function (err, user) {
user.votes = req.body.votes;
user._id = mongoose.Types.ObjectId(user._id);
user.save(function(err, result) {
...
The first is preferred.

Mongoose preventing saving two documents and sub documents

I'm running into an issue using Mongoose, Express where I want to save a sub document to my user by pushing it into the sub document array, which I can do. However the issues arise when I want to delete a gamesession that is stored in the users "sessions" attribute and also delete the gamesession globally. I think the issue arises because I'm saving two seperate instances of a gamesession. Here is the code for creating a new sub document called "gamesession" and pushing it onto the users "session" attribute
//POST /posts
// Route for creating gamesessions for specific user
router.post("/gamesessions/:uID/", function(req, res, next) {
var gamesession = new GameSession(req.body);
req.user.sessions.push(gamesession);
gamesession.postedBy = req.user._id;
req.user.save(function(err, user) {
if(err) return next(err);
gamesession.save(function(err, gamesession){
if(err) return next(err);
res.json(gamesession);
res.status(201);
});
});
});
Here is my UserSchema
var UserSchema = new Schema({
posts: [PostSchema],
sessions: [GameSessionSchema],
email: {
type: String,
unique: true,
required: true,
trim: true
},
username: {
type: String,
unique: true,
required: true,
trim: true
},
password: {
type: String,
required: true
}
});
And my GameSessionSchema
var GameSessionSchema = new Schema({
postedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
region: {
type: String,
required: true
},
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
game: {
type: String,
required: true
},
age: String,
createdAt: {type: Date, default: Date.now},
updatedAt: {type: Date, default: Date.now},
platform: {
type: [String],
enum: ["Xbox One", "PS4", "PC"],
required: true
}
});
Edit: Adding my delete route to see if that helps
//DELETE /posts/:id/comments/:id
//Delete a specific comment
router.delete("/gamesessions/:uID/sessions/:gID", function(req, res) {
var gamesession = new GameSession(req.body);
gamesession.remove(function(err) {
req.user.save(function(err, user) {
if(err) return next(err);
res.json(user);
});
});
});
Then, when I want to delete a gamesession with a route, it only deletes the instance saved in user.sessions and when I want to query all gamesessions, it's still there, but deleted in my User document. Any ideas? I think it's because I'm saving the document twice, and if so, what's the best way to save it in user.sessions while also being able to delete from user.sessions and querying a global session.
Possibly not saving the removed gamesession from the GameSession document?
router.delete("/gamesessions/:uID/sessions/:gID", function(req, res) {
var gamesession = new GameSession(req.body);
gamesession.remove(function(err) {
req.user.save(function(err, user) {
if(err) return next(err);
gamesession.save(function(err, gamesession){
if(err) return next(err);
res.json({message: 'Updated GameSession Doc'}, gamesession)
})
res.json(user);
});
});
});

Manually populating Mongodb field on document creation with Mongoose

Following the Mongoose documentation, I was able to create two docs, but am unable to populate one with the other.
Despite manually setting the 'account' value to reference the other document, my database doesn't seem to create the relation.
Below is the code I've used:
UserAuth.findOne({ email }, (err, user) => {
if (err) return done(err);
if (user) {
return done(null, false,
{ message: 'It appears that email address has already been used to sign up!' });
}
// Create the user account
const newAccount = new UserAccount({
name: {
first: req.body.firstName,
last: req.body.lastName,
},
});
newAccount.save((err) => {
if (err) throw err;
// Create the user authentication
const newAuth = new UserAuth({
email,
account: newAccount,
});
newAuth.password = newAuth.generateHash(password);
newAuth.save((err) => {
if (err) throw err;
return done(null, newAuth);
});
return done(null, newAccount);
});
});
Collections:
User Auth
const UserAuthSchema = new Schema({
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
account: {
type: Schema.Types.ObjectId,
ref: 'User',
},
});
module.exports = mongoose.model('UserAuth', UserAuthSchema);
User Account
const UserSchema = new Schema({
name: {
first: {
type: String,
required: true,
},
last: {
type: String,
required: true,
},
},
team: {
type: Schema.Types.ObjectId,
ref: 'Team',
},
image: {
type: String,
default: 'assets/default_user.png',
},
});
module.exports = mongoose.model('User', UserSchema);
It looks like the part:
// Create the user authentication
const newAuth = new UserAuth({
email,
account: newAccount,
});
should be:
// Create the user authentication
const newAuth = new UserAuth({
email,
account: newAccount._id,
});
And then, when you query the collection, you have to say which field should be populate, as shown in (Mongoose documentation)[http://mongoosejs.com/docs/populate.html]
Ad please check that the types of the 2 linked fields are the same as mentioned in the documentation.

Resources