Mongoose Boolean false vs undefined? - node.js

In mongoose, what is the right way to check the field value is undefined, rather than false?
We have model, which has a Boolean property, but not be initially set. For this reason the possible values are actually: undefined | true | false. Here we need to make the distinction.
The operation is being done from with a NodeJS based application.
An example, as requested:
// Simply schema
const PreferencesSchema = new mongoose.Schema({
forUser: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
allowActionX: {
type: Boolean,
default: undefined
}
});
const Preferences = mongoose.model<any>('Preferences', PreferencesSchema);
const preferences = Preferences.findOne({ user: user });
// if preference.allowActionX is not defined:
// use default behaviour
// else if preference.allowActionX is true
// do action X
// else
// don't do action X

If you want to return a document where a field exists AND is not null, use { a : {$ne: null}}
otherwise you can also check afterword with the === operator to determine if something is false vs undefined
The triple equals, ===, in JavaScript, tests for strict equality. This means both the type and the value we are comparing have to be the same.
if (myvar === false) {
//insert code
}
else if ( myvar === undefined){
//insert code
}
https://codeburst.io/javascript-double-equals-vs-triple-equals-61d4ce5a121a

Related

How to pass the check empty string with enum in mongoose?

How I can ignore the error like validation failed: gender: "" is not a valid enum value for path gender when receive a empty value to save into database
My schema look like:
Person {
gender: {
type: String,
enum: ['men', 'women'],
required: false
}
}
In my code (NodeJS)
I have a check:
if (body.gender && !['men', 'women'].includes(body.gender.toLowercase())) {
throw Error("Gender invalid");
}
await savePerson(body);
If have to lowerCase value to check in case insensitive
I think in Javascript empty string is not same with null or undefined, so the error is not throw. I don't want to check !isNullOrUndefined(body.gender) as well. But in process save to mongo, error validation failed: gender: "" is not a valid enum value for path gender has been threw by mongoose
The first condition in your if is true when body.gender is not null or undefined or empty string "". So these values pass your validation and go to the .save() function
You can change to this, it will catch the invalid values above :
if (!['men', 'women'].includes(body.gender)) {
throw Error("Gender invalid");
}
console.log(!['men', 'women'].includes(undefined)); // true
console.log(!['men', 'women'].includes(null)); // true
console.log(!['men', 'women'].includes("")); // true
Edit: So, the condition is "the gender is string and is one of "men", "women" case insensitive". I suggest this validation:
if (typeof(body.gender) !== "string" || !['men', 'women'].includes(body.gender.toLowerCase())) {
throw Error("Gender invalid");
}
Below I run some test:
function isValidGender(gender) {
if (typeof(gender) !== "string" || !['men', 'women'].includes(gender.toLowerCase())) {
console.log("Error");
} else {
console.log("Valid");
}
}
isValidGender(undefined);
isValidGender(null);
isValidGender("");
isValidGender("Men");
isValidGender("wOmEn");
isValidGender("AA");

Mongoose js validation on sub object multiple fields

I am attempting to validate an entire object (multipel fields) within mongoose.
I have the following custom schema definition:
module.exports.ItemQuantityPair = (ItemModel, strictStacking)=>{
return {
type: {
itemID: { type:Number, required:true },
quantity: { type:Number, min:1, default:1 },
},
validate: {
validator: async value => { //stacking
if(!strictStacking || value.quantity===1) //if no strict stacking, or only quantity of 1 don't validate
return true;
let item = await ItemModel.findOne({itemID:value.itemID}); //otherwise get the item and check if its stackable
if(!item.stackable)
return false; //invalid, quantity > 1 and not stackable
else
return true; //valid, quantity > 1 and stackable
},
message: props=>`${props.value} this item was supplied with a quantity larger than 1, but the item was found to not be stackable. Ensure only stackable items have quantity > 1`
},
}
}
Then later in my schema I use this custom object like so:
const AccountSchema = new mongoose.Schema({
inventory: [ItemQuantityPair(ItemModel, true)] //inventory is a list of ItemQuantity pairs
});
When I try this, I'm presented with the error TypeError: Invalid schema configuration: 'undefined' is not a valid type within the array 'inventory' I assume this is because I can't assign an object tp the type field since these are the only built-in types: https://mongoosejs.com/docs/schematypes.html#what-is-a-schematype. Normally I would validate each field separately, but this specific validation requires both fields' values (quantity and itemID) to be determined as valid or not.
How can I cleanly accomplish validation on multiple fields?
Thanks

Proper way to check a property of Mongoose Document is empty

Suppose a schema
const sch = new mongoose.Schema({
obj: {
subObj: String;
}
});
Then I observe that an non-existing or empty property of a document gives me isEmpty == false.
import { isEmpty } from 'lodash';
// Insert an empty document (i.e. no `obj` property)
Sch.create([{}]);
Sch.findOne({}. (err, doc) => {
// Below gives `{}`
console.log(doc.obj);
// Below gives `false`
console.log(`isEmpty == ${isEmpty(doc.obj)`);
});
I suspect that it is because the document contains obj as its key, i.e. Object.keys(doc).includes('obj') == true or Object.getOwnPropertyNames(doc).includes('obj') == true. But I have no idea to deal with it.
What is a proper way to check emptiness of a mongoose document property ?
Update:
The reason that you are getting that is:
console.log(Object.keys(doc.obj), Object.keys({}));
when running the command above I get: [ '$init', 'subObj' ] [] which means that your Object is not really empty, lodash is probably checking those attributes
You could use something like this:
Sch.findOne({}, (err, doc) => {
if (JSON.stringify(doc.obj) === JSON.stringify({}) ) {
// logic goes here
}
});
I found the solution in mongoose itself. The below does what I tried with lodash
doc.$isEmpty('obj')
Reference
Document.prototype.$isEmpty()

Optional but non-nullable fields in GraphQL

In an update to our GraphQL API only the models _id field is required hence the ! in the below SDL language code. Other fields such as name don't have to be included on an update but also cannot have null value. Currently, excluding the ! from the name field allows the end user to not have to pass a name in an update but it allows them to pass a null value for the name in, which cannot be allowed.
A null value lets us know that a field needs to be removed from the database.
Below is an example of a model where this would cause a problem - the Name custom scalar doesn't allow null values but GraphQL still allows them through:
type language {
_id: ObjectId
iso: Language_ISO
auto_translate: Boolean
name: Name
updated_at: Date_time
created_at: Date_time
}
input language_create {
iso: Language_ISO!
auto_translate: Boolean
name: Name!
}
input language_update {
_id: ObjectId!
iso: Language_ISO!
auto_translate: Boolean
name: Name
}
When a null value is passed in it bypasses our Scalars so we cannot throw a user input validation error if null isn't an allowed value.
I am aware that ! means non-nullable and that the lack of the ! means the field is nullable however it is frustrating that, as far as I can see, we cannot specify the exact values for a field if a field is not required / optional. This issue only occurs on updates.
Are there any ways to work around this issue through custom Scalars without having to start hardcoding logic into each update resolver which seems cumbersome?
EXAMPLE MUTATION THAT SHOULD FAIL
mutation tests_language_create( $input: language_update! ) { language_update( input: $input ) { name }}
Variables
input: {
_id: "1234",
name: null
}
UPDATE 9/11/18: for reference, I can't find a way around this as there are issues with using custom scalars, custom directives and validation rules. I've opened an issue on GitHub here: https://github.com/apollographql/apollo-server/issues/1942
What you're effectively looking for is custom validation logic. You can add any validation rules you want on top of the "default" set that is normally included when you build a schema. Here's a rough example of how to add a rule that checks for null values on specific types or scalars when they are used as arguments:
const { specifiedRules } = require('graphql/validation')
const { GraphQLError } = require('graphql/error')
const typesToValidate = ['Foo', 'Bar']
// This returns a "Visitor" whose properties get called for
// each node in the document that matches the property's name
function CustomInputFieldsNonNull(context) {
return {
Argument(node) {
const argDef = context.getArgument();
const checkType = typesToValidate.includes(argDef.astNode.type.name.value)
if (checkType && node.value.kind === 'NullValue') {
context.reportError(
new GraphQLError(
`Type ${argDef.astNode.type.name.value} cannot be null`,
node,
),
)
}
},
}
}
// We're going to override the validation rules, so we want to grab
// the existing set of rules and just add on to it
const validationRules = specifiedRules.concat(CustomInputFieldsNonNull)
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules,
})
EDIT: The above only works if you're not using variables, which isn't going to be very helpful in most cases. As a workaround, I was able to utilize a FIELD_DEFINITION directive to achieve the desired behavior. There's probably a number of ways you could approach this, but here's a basic example:
class NonNullInputDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
const { args: { paths } } = this
field.resolve = async function (...resolverArgs) {
const fieldArgs = resolverArgs[1]
for (const path of paths) {
if (_.get(fieldArgs, path) === null) {
throw new Error(`${path} cannot be null`)
}
}
return resolve.apply(this, resolverArgs)
}
}
}
Then in your schema:
directive #nonNullInput(paths: [String!]!) on FIELD_DEFINITION
input FooInput {
foo: String
bar: String
}
type Query {
foo (input: FooInput!): String #nonNullInput(paths: ["input.foo"])
}
Assuming that the "non null" input fields are the same each time the input is used in the schema, you could map each input's name to an array of field names that should be validated. So you could do something like this as well:
const nonNullFieldMap = {
FooInput: ['foo'],
}
class NonNullInputDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
const visitedTypeArgs = this.visitedType.args
field.resolve = async function (...resolverArgs) {
const fieldArgs = resolverArgs[1]
visitedTypeArgs.forEach(arg => {
const argType = arg.type.toString().replace("!", "")
const nonNullFields = nonNullFieldMap[argType]
nonNullFields.forEach(nonNullField => {
const path = `${arg.name}.${nonNullField}`
if (_.get(fieldArgs, path) === null) {
throw new Error(`${path} cannot be null`)
}
})
})
return resolve.apply(this, resolverArgs)
}
}
}
And then in your schema:
directive #nonNullInput on FIELD_DEFINITION
type Query {
foo (input: FooInput!): String #nonNullInput
}

Mongoose accepts null for Number field

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.

Resources