Revert to default value in mongoose if field is set to null - node.js

I am using mongoose with nodeJS. Consider the following schema:
var PersonSchema = new mongoose.Schema({
"name": {type: String, default: "Human"},
"age": {type: Number, defualt:20}
});
mongoose.model('Person', PersonSchema);
var order = new Order({
name:null
});
This creates a new Person document with name set to null.
{
name:null,
age: 20
}
Is there anyway to check if the property being created/updated is null and if it is null set it back to default. The following declaration
var order = new Order();
order.name = null;
order.save(cb);
should create a new Person document with name set to default.
{
name:"Human",
age: 20
}
How would one implement this using NodeJs and mongoose.

Well there are a few way to go about this:
PRE-SAVE/PRE-VALIDATE hooks
Mongoose Middleware hooks
Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions.
PersonSchema.pre('save', function(next) {
if (this.name === null) {
this.name = 'Human';
}
next();
});
ENUM
Mongoose Validation and Mongoose Enums
Strings have enum, match, maxlength and minlength validators.
var PersonSchema = new mongoose.Schema({
"name": {type: String, default: "Human", enum: ['Human', 'NONE-Human']},
"age": {type: Number, defualt:20}
});
Update 1
If I have 20 fields where I want to check whether they are set to null, do I have to repeat this.field = default for all of them?
I guess you would have to.
What does NONE-Human in enums do? I could not find documentation for this online.
That is just an example of ENUM with enum you can only choose values that are specified in ENUM i.e. 'Name' can only have values of 'Human' or 'NONE-Human'.

A little late to answer this, but a more generic approach would be not to hardcode the fields again in the pre hook.
Instead let's use the schema itself to go over the fields and check for the fields that have default values and are explicitly set to null. In this case we set it to the default value.
PersonSchema.pre('save', function(next) {
const self = this;
Object.keys(this.schema.paths).forEach(function(key) {
if(self.schema.paths[key].options.default && self[key] === null) {
self[key] = self.schema.paths[key].options.default;
}
});
next();
});

An even later answer. I was looking for a simpler solution and came across this question. Thought I'd share what I ended up doing.
db.Person.create({
name: usersName || "Human",
age: usersAge,
})

Related

Mongoose findOneAndUpdate cast error with custom _id

I have my Person schema like this :
const schema = new mongoose.Schema({
_id: Number,
name: String,
birthday: Date,
sex: String
});
schema.pre('findOneAndUpdate', async function (next) {
try {
let counter = await Counters.findByIdAndUpdate('person',
{
$inc: {
value: 1
}
},
{ new: true}
);
this._update._id = counter.value;
next();
}
catch (err) {
next(err);
}
});
The problem is when I try to add some new persons with findOneAndUpdate and upsert: true, it generates a CastError: Cast to ObjectId failed for value "18" at path "person".
My _id is defined as a Number so I don't understand why it's trying to cast it to an ObjectId ?
Update :
I found my problem, the Person model is referenced in some other model but I forgot to change the ref type in the other model...
person: {
type: Number, //HERE
ref: 'person',
required: true
}
You can change the type of the_id property although ins't a good approach, but actually you can't change the value since it's immutable and represents the primary key of the document. Keep in mind that _id is very important for MongoDB life cycle, like indexing. If you aim to change an Entity key, you can create other property, something like person_id.
_id is an auto generated property for MongoDB. If you want to add try a different name for the Id attribute like "personId" or you can use the auto generated Id by MongoDB without creating a seperate Id.

store values in mongoose by removing comma i.e(abc or 233 or 2344 or 20000)? [duplicate]

I get Date from user in the string format and I currently convert into a Date in controller before creating the Schema object and saving. Is there a way to move this logic to model as it seems to me that Model is the right place for this
var RunSchema = new Schema({
created: {
type: Date,
default: Date.now
},
starttime: {
type: Date,
default: Date.now
}
});
Currently I do this
//req.body = {starttime;'2.05.2013 11:23:22'}
var run = new Run(req.body);
// util.getDate(datetime) returns false if invalid and Date() if valid
// req.body.starttime = '2.05.2013 11:23:22';
run.starttime = util.getDate(req.body.starttime);
run.save(function(err) {
if(err) {
} else {
}
});
On a sidenote, how do I assert if I want to process the param in custom function checks. Something like
req.assert('name', 'Name can\'t be empty').len(1, 1000);
While I'm not sure about the meaning of req.body.starttime, I'm pretty sure you're looking for the Schema objects pre() function which is part of the Mongoose Middleware and allows the definition of callback functions to be executed before data is saved. Probably something like this does the desired job:
var RunSchema = new Schema({
[...]
starttime: {
type: Date,
default: Date.now
}
});
RunSchema.pre('save', function(next) {
this.starttime = new Date();
next();
});
Note that the callback function for the save event is called every time before a record is created or updated. So this is for example the way for explicitly setting a "modified" timestamp.
EDIT:
Thanks to your comment, I now got a better understanding of what you want to achieve. In case you want to modify data before it gets assigned and persisted to the record, you can easily utilize the set property of the Schema:
// defining set within the schema
var RunSchema = new Schema({
[...]
starttime: {
type: Date,
default: Date.now,
set: util.getDate
}
});
Assuming that the object util is within scope (required or whatever) your current implementation fits the signature for the property set:
function set(val, schemaType)
The optional parameter schemaType allows you to inspect the properties of your schema field definition if the transform process depends on it in any way.

Change discriminator value/discriminated type in Mongoose

I've represented a class hierarchy in Mongoose via two models and a discriminator key (simple example):
var options = {discriminatorKey: 'kind'};
var UserSchema = new mongoose.Schema({
username: {type: String, index: true},
// some other fields
}, options);
// some schema methods
var User = mongoose.model('User', UserSchema);
var PowerUserSchema = new mongoose.Schema({
username: {type: String, index: true},
// some other fields
rank: {type: String}
}, options);
var PowerUser = User.discriminator('PowerUser', PowerUserSchema);
So far this works fine, however I ran into the situation, where I would like to "promote" a User to PowerUser. My initial idea was to set the "kind" property of a user and call save() on the instance, hoping that once the value is retrieved next time, the correct mongoose type will be returned:
var user = ... // retrieve user
user.kind = 'PowerUser';
user.save();
user = ... // retrieve user again
This doesn't appear to work, since the "kind" value is not saved to the instance. I came across this suggestion, which unfortunately did not update the discriminator value either.
My question now is: Am I even on the right track? Is updating the discriminator value even allowed for a situation like this, or should I better structure my data in a different way (e.g. use a single schema for both, with a "type" entry specifying what each instance is; this would have the effect that for the demotion case, no information is lost.)
Additionally, pro(de)moting a user should not break all the instances in my database where (Power)Users are referenced.
Thanks!
In the end I got this to work by doing:
var user = ... // retrieve user
var powerUser = PowerUser.hydrate(user.toObject());
powerUser.kind = 'PowerUser';
powerUser.save();
powerUser = ... // retrieve user again
On a side note, demoting a PowerUser back to User does not appear to be working that way.
have you tried using findOneAndUpdate on the Model
User.findOneAndUpdate({_id: _user._id}, {$set: {kind: "PowerUser"}, {new: true}, function (err, doc) {
should.not.exist(err);
should.exist(doc.kind);
doc.kind.should.equal('PowerUser');
done();
});
you could use a static method like this in case you also need to remove properties already set. the value new: true is to get the new modified file and strict: false so you can unset values that dont already exist on UserSchema
changes = {kind: "PowerUser"}
UserSchema.statics.switchKind = function (id, changes, callBack) {
const unset = {
rank: undefined,
someOtherField: undefined
};
return this.findOneAndUpdate({_id: id}, {$set: changes, $unset: unset}, {new: true, strict: false}, callBack);
};
As of Mongoose 6, you can modify the value of the discriminator key by using the overwriteDiscriminatorKey option:
For some reason it wasn't validating correctly for me with any of the "update" methods (e.g. findByIdAndUpdate) (bug report), so I had to do a replace, which means you need to provide the whole document:
const user = ... // retrieve user
const newUser = {
...user.toObject(), ...{ kind: 'PowerUser', name: 'New Name' }
};
User.replaceOne({ _id: user._id }, newUser, {
overwriteDiscriminatorKey: true,
runValidators: true
});
This will validate it against the PowerUser schema to make sure your document is valid.

How can I make a mongo document filed read-only in mongoose schema field declaration? [duplicate]

Short and clear: is there any way to prevent setting a schema field but allowing to get the value?
I've been around the Mongoose Documentation but can't find what I'm looking for.
An alternative if you want to set a default value that can never be changed:
var schema = new Schema({
securedField: {
type: String,
default: 'Forever',
set: function (val) { return this.securedField; }
});
Define the field as a virtual getter instead of a traditional field.
For example, say you wanted to make the pop field of your collection read-only when accessed via Mongoose:
var schema = new Schema({
city: String,
state: String
});
schema.virtual('pop').get(function() {
return this._doc.pop;
});
By accessing the private _doc member of your model instance it's possible this may break in the future, but this worked fine when I tested it just now.
Since mongoose 5.6 you can do: immutable: true
var schema = new Schema({
securedField: {
type: String,
default: 'Forever',
immutable: true
}
});
You can just return from set the same value as the default value, no need to reference the _this document:
var schema = new Schema({
securedField: {
type: String,
default: 'Forever',
set: () => 'Forever'
});

Is it possible to create a multi-select enum in mongoose

I have a model with an enum field, and currently documents can have any single value from the enum. I want documents to be allowed to have an array of values, but have mongoose enforce that all of the values are valid options that exist in the enum - is that possible?
Essentially I want the equivalent of a HTML <select multiple> element instead of a <select>
Yes, you can apply an enum to a path that's defined as an array of strings. Each value will be passed to the enum validator and will be checked to ensure they are contained within the enum list.
var UserSchema = new Schema({
//...
pets: {type: [String], enum: ["Cat", "Dog", "Bird", "Snake"]}
//...
});
//... more code to register the model with mongoose
Assuming you have a multi-select in your HTML form with the name pets, you could populate the document from the form post, within your route, like this:
var User = mongoose.model('User');
var user = new User();
//make sure a value was passed from the form
if (req.body.pets) {
//If only one value is passed it won't be an array, so you need to create one
user.pets = Array.isArray(req.body.pets) ? req.body.pets : [req.body.pets];
}
For reference, on Mongoose version 4.11 the enum restriction on the previous accepted answer does not work, but the following does work:
const UserSchema = new Schema({
//...
role: [ { type: String, enum: ["admin", "basic", "super"] } ]
//...
})
Or, if you want/need to include things other than type:
const UserSchema = new Schema({
//...
role: {
type: [ { type: String, enum: ["admin", "basic", "super"] } ],
required: true
}
//...
})

Resources