Setting mongoose to allow null without breaking validation - node.js

Here is how my application works. A user logs in for the first time using Google Sign in. We get the following data from their Google Account:
Given name
Family name
Email ID
We wish to use this information to call our API (POST request) to create a user profile.
The data we send is
{
firstName: firstName ,
lastName: lastName,
email: email
}
Here is where the issue comes from. The user profile has many fields and one of them is designation. When the user logs in for the first time, we don't know their designation.
We are using MongoDB for our database. So we use Mongoose to set up the connection. In Mongoose model, we have added some validation for our schema. Designation is a required field. It should be at least one character of length and maximum of 40 characters. If we set designation as null, the validation would fail.
Is there any way to allow null in a required field in Mongoose?

Rather than setting required to true or false, you can pass it a function:
const user = new Schema({
designation: {
type: String,
minLength: 1,
maxLength: 40,
required: function() {
// Rather than checking a stored variable, you could check
// static functions on the model, a custom value on the
// instance that isn't persisted, etc.
return this.hasLoggedInAtLeastOnce === true;
// If this function returns true, the field is required.
}
}
hasLoggedInAtLeastOnce: {
type: Boolean,
}
});

Related

Duplicate null key value error MongoDB with Mongoose

I'm trying to add a user to my users collection and keep getting a duplicate null key value error.
My Users model used to look like this like this:
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
profilePictures: [{
link: {
type: String
}
rank: {
type: Number,
unique: true
}
}],
});
module.exports = User = mongoose.model("users", UserSchema);
Before I changed the pictures field to
...
pictures = []
...
I believe because I saved users under the former schema, it has saved somewhere the model of the object in the pictures array (they would be given an ObjectId when I saved something to that array).
Even though I have changed the field to
pictures = []
I still get this error
E11000 duplicate key error collection: testDB.users index: profilePictures.rank_1 dup key: { profilePictures.rank: null }
When neither profilePictures nor rank fields even exist anymore.
I imagine I can probably just delete the users collection and start again but I want to know if there is a better way to handle this error? Suppose I had 100 users in the users collection – I wouldn't be able to just delete them all.
Thanks!
you added unique property true in your model to profilePictures.rank. on first request it is saving null as you may be not providing rank in your query.
second time it is again trying to save null but it is also marked unique so it is throwing exception.
solution:
remove unique flag from profilePictures.rank
provide unique value for profilePictures.rank

How can I 'resolve' a MogoDB object to a specific JSON structure in Nodejs?

Suppose the following User Schema in MongoDB (using Mongoose/Nodejs):
var UserSchema = new Schema({
email: {
type: String,
unique: true,
required: 'User email is required.'
},
password: {
type: String,
required: 'User password is required.'
},
token: {
type: String,
unique: true,
default: hat
},
created_at: {
type: Date,
default: Date.now
},
});
// mongoose-encrypt package
UserSchema.plugin(encrypt, {
secret: 'my secret',
encryptedFields: ['email', 'password', 'token', 'created_at']
});
Now assume I want to return the user object from an API endpoint. In fact, suppose I want to return user objects from multiple API endpoints. Possibly as a standalone object, possibly as a related model.
Obviously, I don't want password to be present in the returned structure - and in many cases I wouldn't want token to be returned either. I could do this manually on every endpoint, but I'd prefer a no-thought solution - being able to simply retrieve the user, end of story, and not worry about unsetting certain values after the fact.
I mainly come from the world of Laravel, where things like API Resources (https://laravel.com/docs/5.6/eloquent-resources) exist. I already tried implementing the mongoose-hidden package (https://www.npmjs.com/package/mongoose-hidden) to hide the password and token, but unfortunately it seems as though that breaks the encryption package I'm using.
I'm new to Nodejs and MongoDB in general - is there a good way to implement this?
How to protect the password field in Mongoose/MongoDB so it won't return in a query when I populate collections?
You can use this: Users.find().select("-password"),
but this is done whenever you send the queried item to the user (res.json()...) so you can do your manipultions with this field included and then remove it from the user before you send it back (this is using the promise approach, the best practice).
And if you want your changes to be used as default you can add "select: false" into the schema object's password field.
Hope this helps :)

Default value not set while using Update with Upsert as true

I have the following model for users:
var UserSchema = new mongoose.Schema({
name: String,
dob: Date,
sex: String,
photo: String,
email: {type: String, index: {unique: true, required: true}},
created: {type: Date, default: Date.now}
});
var User = mongoose.model('Users', UserSchema);
As you can see the 'created' field takes a default value of the current date so that it is automatically set when a new user is created.
I use the following query when user details are posted:
User.findOneAndUpdate({email: user.email}, user, {upsert: true}, function (err, user) {
if (err) {
return callback (err);
} else {
callback(null, user);
}
});
The purpose of using findOneAndUpdate with upsert: true is to either return an existing profile, or create a new one. It also updates any fields based on the data posted.
However, the created field gets updated with the current date each time, even though the created field is not posted. How can I make sure that this field is set only once?
EDIT
An example object from the database:
{
"_id" : ObjectId("54620b38b431d48bce7cab81"),
"email" : "someone#google.com",
"__v" : 0,
"name" : "somone",
"sex" : "male"
}
It turns out that the created field is not being set even while creating a new object using upsert. Mongoose just returns the current date based on the schema even though it does not exist in the document.
So, the question now becomes: How do I make sure that using upsert creates the default value for a field not supplied in the arguments?
For adding defaults to your document if it was created with findOneAndUpdate (it didn't exist before the query) and you did not provide the field in the update you should use setDefaultsOnInsert.
When upsert and setDefaultsOnInsert are both true, the defaults will be set if the record is not found and a new one is created.
This skips the workflow of having to check if the record exists and if not then creating a new one with 'save' just to make sure defaults are set.
I have had the same issue (record created with findOneAndUpdate with upsert: true) and the default value for a field was not added to the record, even though it was in the schema.
This is only in regards to adding defaults when using findOneAndUpdate to create documents, not for skipping the update of the 'created' field.
e.g.
User.findOneAndUpdate({email: user.email}, user, {upsert: true, setDefaultsOnInsert:true}, ...)
findOneAndUpdate simply sends a MongoDB findAndModify request (see findOneAndUpdate). What this means is that it skips all the mongoose magic involved with the schema setters, getters, defaults, etc. Validation is only run on create/save so the way around this is to do a .findOne(), check existence/create a new one, and then .save().
see this issue for more discussion
EDIT:
In regards to the first question about changing the date each time, you could change the schema a bit. Get rid of the default value, and instead add this after declaring the schema:
UserSchema.pre("save", function (next) {
if (!this.created) {
this.created = new Date();
}
next();
});
That will only create a date if the created: value is not present, and should prevent it from changing the creation date each time (when using .save()).
see Mongoose middleware

MongoDB collection/Express.js for "friend request" functionality

I have an idea for how to store the relationships. Each user has a friends Array filled with IDs. However, how should I initiate a friend request in my Express.js app in MongoDB?
I'm thinking about creating a "notifications" collection with:
_id, userId, type, friendId, read
So when the requested friend logs in, they can see all of their own notifications to deal with...
Or is that ridiculous?
For such notifications what I did, is as follows:
var notificationSchema = mongoose.Schema({
to:{
type: mongoose.Schema.Types.ObjectId, ref: 'User'
},
from:{
type: mongoose.Schema.Types.ObjectId, ref: 'User'
},
for: {
type: String
},
status: {
type: String,
default: "Not Seen"
},
description:{
type: String
}
},{ timestamps: { createdAt: 'created_at' }
});
where I have saved _id of to and from (users) where I used for field for notification type like following, liked etc and used description field as optional.
Sounds reasonable enough. But my approach will be a little different. I would store the notifications in the user db itself. Something like
username
dob
...
notifications[[type: 'friend', read: 0, request_from: '22sd300sdf45003425asz'], ...]
...
This way, you don't have to make a db call on every page load. As soon as you initialize a session (I use passport), it will be there, ready already for templates. After a valid action from the user, I can delete it or whatever.
But again, its dependent on the need. Do what suits you best!
If you store it in the user passport session (solution mentioned earlier) you will not be able to receive anymore notifications since it is static information in the header and not connected directly to the document store.
The best way to do it would to have let it have it's own store.
{Id:ObjectId, for_user:String, sent from:String, status:Boolean}
Perhaps you can initially set the status to null then set it to true or false when a user accepts or denies it. After create the user to user friend relationship. That's more or less the way I would go about it.

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