Mongoose accepts null for Number field - node.js

I have a mongoose schema where I'm storing a port number. I also have a default value set for the field.
port:{
type:Number,
default:1234
}
If I don't get any value via my API, it gets set to 1234.
However, If someone sends null, it accepts null and saves to database.
Shouldn't it covert null to 1234? null is not a number! Am I understanding it wrong?
I am considering the solution given here, but I dont want to add extra code for something that should work without it (unless I'm wrong and its not supposed to convert null to 1234)

See the comments in this issue:
https://github.com/Automattic/mongoose/issues/2438
null is a valid value for a Date property, unless you specify required. Defaults only get set if the value is undefined, not if its falsy.
(it's about dates but it can be applied to numbers just as well.)
Your options are to either:
add required to the field
add a custom validator that would reject it
use hooks/middleware to fix the issue
You might get away with a pre-save or post-validate (or some other) hook like this:
YourCollection.pre('save', function (next) {
if (this.port === null) {
this.port = undefined;
}
next();
});
but probably you'll have to use something like:
YourCollection.pre('save', function (next) {
if (this.port === null) {
this.port = 1234; // get it from the schema object instead of hardcoding
}
next();
});
See also this answer for some tricks on how to make null trigger default values in function invocation:
Passing in NULL as a parameter in ES6 does not use the default parameter when one is provided
This is unfortunate that Mongoose cannot be configured to tread null as undefined (with some "not-null" parameter or something like that) because it is sometimes the case that you work with data that you got in a request as JSON and it can sometimes convert undefined to null:
> JSON.parse(JSON.stringify([ undefined ]));
[ null ]
or even add null values where there was no (explicit) undefined:
> JSON.parse(JSON.stringify([ 1,,2 ]));
[ 1, null, 2 ]

As explained in mongoose official docs here
Number
To declare a path as a number, you may use either the Number global constructor or the string 'Number'.
const schema1 = new Schema({ age: Number }); // age will be cast to a Number
const schema2 = new Schema({ age: 'Number' }); // Equivalent
const Car = mongoose.model('Car', schema2);
There are several types of values that will be successfully cast to a Number.
new Car({ age: '15' }).age; // 15 as a Number
new Car({ age: true }).age; // 1 as a Number
new Car({ age: false }).age; // 0 as a Number
new Car({ age: { valueOf: () => 83 } }).age; // 83 as a Number
If you pass an object with a valueOf() function that returns a Number, Mongoose will call it and assign the returned value to the path.
The values null and undefined are not cast.
NaN, strings that cast to NaN, arrays, and objects that don't have a valueOf() function will all result in a CastError.

Related

Yup validation convert empty string to default value

In my Yup schema I was my String field name to allow you to pass in any string, an empty string, or nothing at all. If you pass in a string, it passes. If you pass in an empty string or nothing, I want to convert to a default value.
This is the schema that I thought would cover it:
const mySchema = yup.object().shape({
name: yup.string('Name must be a string').max(100, 'Name has a max of 100 characters').default('John Doe')
});
However, if I pass in an empty string '', it does not trigger the default conversion and it just passed through as an empty string. I've tried adding required() but that just makes the line fail if I pass an empty string. I've tried nullable() and trim() but nothing seems to work.
How can I make the default value replace empty strings?
I ended up adding a simple method to transform empty string to undefined which will get picked up in the default:
// Add method
yup.addMethod(yup.string, 'stripEmptyString', function () {
return this.transform((value) => (value === '' ? undefined : value));
});
// Usage
const mySchema = yup.object().shape({
name: yup.string('Name must be a string').stripEmptyString().default('John Doe')
});

Joi "or" operator thinks an empty string is a valid input

I am trying to use the "or" operator in Joi ver.17.4.0
As you can see, in the code below, I want either or both of the attributes/properties to be allowed, but not neither.
The problem is that Joi does not allow a string to be empty. So, to have it empty, I need to:
Joi.string().allow('')
This makes it not empty according to the "or" operator. So I can not get the 'name' to be empty in the eyes of "or".
It won't validate properly.
It validates even when I do this (but it shouldn't):
validatePerson(createPerson(''));
Keep in mind that I'm actually validating POST input on a node express API, so this is some simplified code to illustrate the issue:
const Joi = require('Joi');
function createPerson(name, age) {
const person = { name: name, age: age };
console.log(person);
return person;
}
function validatePerson(person) {
const schema = Joi.object({
name: Joi.string().allow(''),
age: Joi.number(),
}).or("name", "age");
console.log(schema.validate(person));
return schema.validate(person);
}
validatePerson(createPerson('')); // This should fail validation but doesn't
validatePerson(createPerson()); // Should fail and does
validatePerson(createPerson('Bob')); // Should pass and does
validatePerson(createPerson('', 7)); // Should pass and does
validatePerson(createPerson('Bob', 7)); // Should pass and does
As far as I understand, you want to allow the name to be empty an string, only if the age exists.
To acheive that, you can use .when:
name: Joi.string().when('age', { is: Joi.exist(), then: Joi.allow('') })
This way, your first example will fail as you expected.

`parseValue` are not called for input parameter of a customised scalar type

I define a schema like this:
const query = new GraphQLObjectType({
name: 'Query',
fields: {
quote: {
type: queryType,
args: {
id: { type: QueryID }
},
},
},
});
const schema = new GraphQLSchema({
query,
});
The QueryID is a customised scalar type.
const QueryID = new GraphQLScalarType({
name: 'QueryID',
description: 'query id field',
serialize(dt) {
// value sent to the client
return dt;
},
parseLiteral(ast) {
if (ast.kind === 'IntValue') {
return Number(ast.value);
}
return null;
},
parseValue(v) {
// value from the client
return v;
},
});
client query
query {
quote(queryType: 1)
}
I found that the parseValue method is not called when clients send query to my server. I can see parseLiteral is called correctly.
In most of the document I can find, they use gql to define schema and they need to put scalar QueryID in their schema definition. But in my case, I am using GraphQLSchema object for schema. Is this the root cause of that? If yes, what is the best way to make it works? I don't want to switch to gql format because I need to construct my schema at runtime.
serialize is only called when sending the scalar back to the client in the response. The value it receives as a parameter is the value returned in the resolver (or if the resolver returned a Promise, the value the Promise resolved to).
parseLiteral is only called when parsing a literal value in a query. Literal values include strings ("foo"), numbers (42), booleans (true) and null. The value the method receives as a parameter is the AST representation of this literal value.
parseValue is only called when parsing a variable value in a query. In this case, the method receives as a parameter the relevant JSON value from the variables object submitted along with the query.
So, assuming a schema like this:
type Query {
someField(someArg: CustomScalar): String
someOtherField: CustomScalar
}
serialize:
query {
someOtherField: CustomScalar
}
parseLiteral:
query {
someField(someArg: "something")
}
parseValue:
query ($myVariable: CustomScalar) {
someField(someArg: $myVariable)
}

Increment a column with the default value as null

I need to increment a column with 1 on some occasions, but the default value of that column is null and not zero. How do I handle this case using sequelize? What method could be utilized?
I could do by checking the column for null in one query and updating it accordingly in the second query using sequelize but I am looking for something better. Could I handle this one call?
I'll confess that I'm not terribly experienced with sequelize, but in general you'll want to utilize IFNULL. Here's what the raw query might look like:
UPDATE SomeTable
SET some_column = IFNULL(some_column, 0) + 1
WHERE <some predicate>
Going back to sequelize, I imagine you're trying to use .increment(), but judging from the related source, it doesn't look like it accepts anything that will do the trick for you.
Browsing the docs, it looks like you might be able to get away with something like this:
SomeModel.update({
some_column: sequelize.literal('IFNULL(some_column, 0) + 1')
}, {
where: {...}
});
If that doesn't work, you're probably stuck with a raw query.
First you need to find the model instance and update via itself, or update directly via Sequelize Static Model API.
Then you'll check whether the updated field got nullable value or not ? If fails then do the manual update as JMar propose above
await model.transaction({isolationLevel: ISOLATION_LEVELS.SERIALIZABLE}, async (tx) => {
const user = await model.User.findOne({
where: {
username: 'username',
},
rejectOnEmpty: true,
transaction: tx,
});
const updatedRecord = await user.increment(['field_tag'], {
transaction: tx,
});
if (!updatedRecord.field_tag) {
/** Manual update & Convert nullable value into Integer !*/
await model.User.update({
field_tag: Sequelize.literal('IFNULL(field_tag, 0) + 1')
}, {
where: {
username: 'username',
},
transaction: tx,
});
}
});

Mongoose update dependent field from another field's setter?

I have a scenario in node/express/mongoose where I have users who accumulate points, and when that point total crosses a certain threshold, they "level up" (think games with point-based levels).
I have created a custom setter on the points field that checks if the value has changed, and if so tries to update the level field. Levels are defined in another collection, but are saved as simple JSON objects when associated with user docs (hence the .lean() in the query). I did it this way vs a virtual field or population for efficiency.
Problem: this doesn't actually seem to update the user 'level' field when it should. What am I doing wrong?
// Define our user schema
var UserSchema = new mongoose.Schema({
...
points: {type: Number, default: 0, set: pointsChangeHandler},
level: {name: String, minPoints: Number, maxPoints: Number},
...
});
And the setter looks like so:
function goodPointsChangeHandler(newValue) {
var self = this;
if (newValue != self.goodPoints) {
console.log('Point change detected.');
// Find level that corresponds with new point total
Level.findOne({
'minPoints': {$lte : self.goodPoints},
'maxPoints': {$gt : self.goodPoints}}, '-_id').lean().exec(function(err, level) {
if (self.goodLevel == undefined || self.goodLevel.rank != level.rank) {
console.log('Level changed.');
self.goodLevel = level;
}
return newValue;
});
}
return newValue;
}
Based on #laggingreflex's comment, I tried modifying this within the scope of the model method (i.e. not in the Level.findOne() callback, and changes made that way were persisted without an explicit save() call.
Also, I had a pretty silly error where I was returning newValue from thefindOne` callback.. not sure what I was thinking there...
Long story short, and this may be obvious to node/express/mongoose experts, but you can modify fields other than the one whose setter method you're currently in, but the moment you find yourself in the callback of another async method, you'll have to do an explicit save() or your modifications to this will not be persisted.
So:
function myFieldSetterMethod(newValue) {
this.myField = "a";
this.myOtherField = "b";
return newValue;
// no save() necessary, this will update both fields
}
function myFieldSetterMethod(newValue) {
this.myField = "a";
SomeModel.findOne(..., function(err, doc) {
this.myOtherField = doc.somethingIWantFromThisDoc;
// now you'll need an explicit save
// this.save(...)
});
return newValue;
}

Resources