MongoDB & Mongoose: unable to populate a user's posts with .populate() - mongodb

I've searched this site for days looking through the many different but similar questions on this topic to no avail.
Here's what I'd like to happen. A user signs in and their posts are automatically linked to the users collection. Eventually I'd like to link posts to the profile it was posted to, but i"m not quite there yet. Here's what I've tried so far.
In the User Schema:
const UserSchema = new Schema({
posts: [{
type: Schema.Types.ObjectId,
ref: 'posts'
}],
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
...
});
module.exports = User = mongoose.model('users', UserSchema);
In the Post Schema:
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
text: {
type: String,
required: true
},
name: {
type: String
},
...
});
module.exports = Post = mongoose.model('posts', PostSchema);
In my users api, here's how I'm signing the user in and attempting to populate the user's posts:
const User = require('../../models/User');
router.post('/login', (req, res) => {
const { errors, isValid } = validateLoginInput(req.body);
// Check Validation
if (! isValid) {
return res.status(400).json(errors);
}
const email = req.body.email;
const password = req.body.password;
// Find user by email
User.findOne({ email })
.populate('posts')
.then(user => {
if (! user) {
errors.email = 'User not found';
return res.status(400).json(errors);
}
// Check password
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
// User Matched
// Create JWT Payload
const payload = {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
name: user.firstName + ' ' + user.lastName,
avatar: user.avatar,
posts: user.posts
};
jwt.sign(
payload,
keys.secretOrKey,
{ expiresIn: 3600 }, (err, token) => {
res.json({
success: true,
token: 'Bearer ' + token,
payload
});
});
} else {
errors.password = 'Password is incorrect';
return res.status(400).json(errors);
}
});
});
});
In the posts api, here's how the post is being submitted:
router.post('/', passport.authenticate('jwt', { session: false }), (req, res) => {
const { errors, isValid } = validatePostInput(req.body);
if (! isValid) {
// Return errors with 400 status
return res.status(400).json(errors)
}
const newPost = new Post({
text: req.body.text,
name: req.body.name,
avatar: req.body.avatar,
user: req.user.id
});
newPost.save().then(post => res.json(post));
});
Currently, all I'm seeing is an empty array and no errors. I've been spinning my wheels on this one for a couple days now so any help would be appreciated. Thanks!

I think you forgot to save the _id of your new post to the User model so that the populate() can lookup the posts to populate:
newPost.save().then(post => {
User.update({ _id: req.user.id }, { $push: { posts: post._id }}, (err) => {
res.json(post));
});
});

Related

How to update a user profile which has a property which is a ref in MongooseJS?

I have a User schema which has reference to a profile schema.
const UserSchema = new Schema(
{
_id: mongoose.Schema.Types.ObjectId,
email: {
....email props...
},
password: {
...password props...
},
profile: [{
type: mongoose.Schema.Types.ObjectId,
ref: "Profile",
}],
},
);
const Profile = new Schema({
_user: {
type: Schema.Types.ObjectId, ref: 'User'
},
'displayName': {
type: String,
default: ''
},
'interestedActivities': ['Ping-pong'], <---- This bad boy/girl is an array
'memberSince': { type: Date, default: Date.now }
}
)
I'd like to create a route which can update the User properties AND the Profile properties in one shot—with a caveat one of the properties on the Profile model is an array!!!
I tried this....
handler
.use(auth)
.put((req, res, next) => {
emailValidator(req, res, next, 'email');
},
async (req, res, next) => {
await connectDB()
const {
profileDisplayName,
profileEmail,
interestedActivities } = req.body;
const update = {
email: profileEmail,
'profile.$.displayName': profileDisplayName,
'profile.$.interestedActivities': interestedActivities
}
const filter = { _id: req.user.id };
const updatedUser = await User.findOneAndUpdate(filter, update, { new: true })
try {
console.log("updatedUser ", updatedUser);
if (updatedUser) {
return res.status(200).send({
updatedUser,
msg: `You have updated your profile, good job!`
});
}
} catch (error) {
errorHandler(error, res)
}
})
export default handler;
My response is:
Status Code: 500 Internal Server Error
Cast to ObjectId failed for value "[
{
id: 'ae925393-0935-45da-93cb-7db509aedf20',
name: 'interestedActivities',
value: []
}
]" (type Array) at path "profile.$"
Does anyone know how I could also afford for the property which is an array?
Thank you in advance!

How create relationship between two collections

I'm creating server app using nodejs(express) and mongodb(mongoose). I must create relationships between Organization model and Users model. After creating an organization, I want to create a user that will apply to a specific organization. One user can apply to many organizations. How can I do this?
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
// UserShema
const UserSchema = Schema({
login: {
type: String,
require: true,
unique: true
},
password: {
type: String,
require: true
},
organization: {
ref: "Organization",
type: Schema.Types.ObjectId
}
});
// Organization Schema
const OrganizationSchema = Schema({
label: {
type: String
},
users: [{
type: Schema.Types.ObjectId,
ref: "Users"
}]
});
//For now I have simple route for creating an Organization.
// request:
// {
// "label": "testOrg"
// }
exports.createOrganization = async (req, res) => {
try {
const org = await new Organization(req.body);
await org.save();
} catch (error) {
return res.status(500).json({error})
}
}
//And I have this route for user registration
exports.signup = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
};
const {login} = req.body;
try {
const checkUser = await Users.findOne({login});
if (!checkUser) {
const user = await new Users(req.body);
await user.save();
return res.status(200).json({ user });
} else {
return res.status(400).json({error: "User already exist"})
}
} catch (error) {
return res.status(500).json({error})
}
};
You could embed the organization id into a string into the user document
Like this {
name: "Name",
location: "CA",
organizations: [123456789, 234567890, ...]
}

GraphQL User Authentication

I'm trying to Query for a user and return the user only if the username and password match what's in the database. Currently if I query with the username, I get the response no matter what, doesn't matter if the password is right or wrong. (Read bottom for context) This is what my graphql schema is:
signInUser: {
type: UserType,
args: {
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(parent, args) {
return User.findOne({ username: args.username }, (err, user) => {
if (bcrypt.compareSync(args.password, user.password)) {
return user;
} else {
return null;
}
});
}
}
This is my UserType schema:
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: GraphQLID },
username: { type: GraphQLString },
posts: {
type: new GraphQLList(PostType),
resolve(parent, args) {
// Matches posts based on username
// return _.filter(posts, { name: parent.name });
console.log(parent.name);
return Post.find({ name: parent.name });
}
}
})
});
My MongoDB schema:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
username: String,
password: String
});
module.exports = mongoose.model('User', userSchema);
// model = collection in database
Should I be using JWT? I'm also having trouble wrapping my mind of how to handle knowing that the user has logged in on my client side of the application. I was thinking this way, if this query returned empty I could assume that the login failed, otherwise store the user id into localStorage and use it whenever. Suggestions of other approaches would be greatly appreciated!

Mongoose - how to move object to another collection

My db include following collections:
users
deleted_users
My code is following:
const name = { type: String, required: true, index: { unique: true } };
const UserSchema = new mongoose.Schema({ name });
const DeletedUserSchema = new mongoose.Schema({ name }, {
versionKey: 'version',
});
const UserModel = mongoose.model('User', UserSchema);
const DeletedUserModel = mongoose.model('Deleted_user', DeletedUserSchema);
router.put('/:id/move', (req, res) => {
UserModel.findOne(
{ _id: id }
).then((user) => {
if (!user) {
return fail(...);
}
console.log(`moving user width id ${id}`);
const newUser = new DeletedUserModel(user);
return newUser.save()
.then(
() => {
console.log('ok');
})
.catch((err) => {
console.log('catch err ', err);
});
});
}
but I always receive
{ Error
at model.wrappedPointCut [as save] (/~/prj/node_modules/mongoose/lib/services/model/applyHooks.js:111:29)
at UserModel.findOne.then (/~/prj/src/routes/user/index.js:123:20)
at process._tickDomainCallback (internal/process/next_tick.js:135:7)
message: 'No matching document found for id "58dd804c434bdc1848d491cd"',
name: 'VersionError' }
Can you check that this id you are querying is not a String but an ObjectId because I think you are passing a String as id.

Mongoose User model for handling local and social auth providers

So I am working on an Express (with Mongoose and Passport) app and I want to include facebook as an authentication method. I have already done it and it works but I don't think I have a proper User model to handle the authentication process for multiple social providers. I want to merge the different social accounts. Here is my current user model which works for facebook auth:
let userSchema = mongoose.Schema({
email: { type: String, unique: true },
name: { type: String },
password: { type: String },
facebookId: String,
facebookToken: String
}, { timestamps: true });
I think of the following two approaches but I am not sure if they are viable and which will be the most flexible and independent from the social provider.
I am thinking of having local and social arrays like this:
let userSchema = mongoose.Schema({
local: {
email: { type: String, unique: true },
name: { type: String },
password: { type: String },
},
facebook: {
id: String,
token: String,
email: String,
name: String
},
google: {
id: String,
token: String,
email: String,
}
}, { timestamps: true });
The third approach is just overwriting the social provider id (I am not sure if this is okay).
let userSchema = mongoose.Schema({
email: { type: String, unique: true },
name: { type: String },
id: String,
token: String,
}, { timestamps: true });
So I found a working solution for myself which might help other people with the same problem. In my User model I have my usual fields and for each social provider I have a separate array like so (users/User.js):
let userSchema = mongoose.Schema({
email: { type: String, unique: true },
name: { type: String },
password: { type: String },
roles: [String],
confirmation_code: String,
confirmed: { type: Boolean, default: false },
facebook: {
id: String,
token: String,
email: String,
name: String
},
google: {
id: String,
token: String,
email: String,
name: String
}
}, { timestamps: true });
When authenticating with a social provider I make an extra check if a user with the same email already exists. If it doesn't, I create a new user. If it does I just add the social provider data (id, token, etc.) to the already existing users array like so (config/passport.js):
passport.use(new FacebookStrategy({
clientID: oauth.facebook.clientID,
clientSecret: oauth.facebook.clientSecret,
callbackURL: oauth.facebook.callbackURL,
profileFields: ['id', 'emails', 'name']
},
function (accessToken, refreshToken, profile, done) {
process.nextTick(function () {
User.findOne({
$or: [
{ 'facebook.id': profile.id },
{ 'email': profile.emails[0].value }
]
}, function (err, user) {
if (err) {
return done(err);
}
if (user) {
if (user.facebook.id == undefined) {
user.facebook.id = profile.id;
user.facebook.token = accessToken;
user.facebook.email = profile.emails[0].value;
user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
user.save();
}
return done(null, user);
} else {
let newUser = new User();
newUser.facebook.id = profile.id;
newUser.facebook.token = accessToken;
newUser.facebook.email = profile.emails[0].value;
newUser.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
newUser.name = profile.name.givenName + ' ' + profile.name.familyName;
newUser.email = profile.emails[0].value;
newUser.save(err => {
if (err) {
console.log(err);
throw err;
}
return done(null, newUser);
});
}
});
});
}
));
With this approach you can connect one profile with multiple social providers. However there is one downside. If the user registers a new profile for the first time through a social provider, he won't have a password because social providers don't give back password data (duh). He just needs to change (set) his password through his profile afterwards.