Mongoose OR schema validation - node.js

I have a field which will be one of two objects (either a stored credit card or a given credit card):
payment_method:
cc_token: String
security_code: String
payment_method:
number: String
security_code: String
expiration_month: Number
expiration_year: Number
billing_address:
_type: String
first_name: String
last_name: String
address_line1: String
address_line2: String
zip_code: String
city: String
state: String
phone_number: String
I know the passed data is going to match one of these, but not both. Is there a way of specifying some sort of OR construct for the validation?

You didn't provide examples of your containing schema, but, there are a number of ways to validate.
One thing I did was specified the "mixed" type for the Schema allowing any type to be used for the field that could contain either type.
function validatePaymentMethod(value) {
if (!value) { return false; }
// put some logic that checks for valid types here...
if (value.cc_token && value.billing_address) {
return false;
}
return true;
}
var OrderSchema = new mongoose.Schema({
payment_method : { type: mongoose.Schema.Types.Mixed,
validate: [validatePaymentMethod, 'Not valid payment method'] }
});
var Order = mongoose.model("Order", OrderSchema);
var o = new Order();
o.payment_method = { cc_token: 'abc', billing_address: 'Street' };
o.validate(function(err) {
console.log(err);
});
The others are documented here.

Related

Add/Remove users to an array of ObjectId Fails

Thank you for taking out time to read this.
I have two models positions and users. I'm trying to add 'users' to the Array of 'Recruiters' as seen below in positions Model. When I make the put request, Everything goes well but my amended array which includes the new userids fail to save and give the following error.
'Cast to [ObjectId] failed for value "[3]" at path "recruiters"'
PositionsModel.js
const positionsSchema = new Schema(
{
title: {
type: String,
required: [true, 'Position title is required'],
},
description: {
type: String
},
recruiters: [{
type: Schema.Types.ObjectId,
ref: "users"
}]
},
{ timestamps: true }
);
usersModel.js
const usersSchema = new Schema(
{
name: {
type: String,
required: [true, "Name must be provided"],
},
email: {
type: String
},
password: {
type: String,
}
},
{ timestamps: true }
);
Controller.js (Problem Here)
I'm making a put request to add recruiter to the Array in Positions Model and sending two parameters. Id (this is the position id) and Recruiter (this is the userId)
exports.addRemove = async (req, res, next) => {
try {
const {id, recruiter} = req.params;
//Get Current Position Details
const position = await positionsModel.findById(id)
// Update Position
const newList = position.recruiters.push(recruiter) //this works, It adds the id to array
const newData = {
recruiters: newList
}
//At this point if you console log position.recruiters. You will see the newly added item in the array
const uptPosition = await positionsModel
.findByIdAndUpdate(id, newData, {
new: true,
runValidators: true,
})
.exec(); // this fails with error
if(!uptPosition) {
return res.status(400).json("Position failed to update");
}
//Success Response
return res.status(200).json('Position Updated');
} catch (err) {
console.log({ Error: err.message });
return;
}
Current List of Recruiter Id's in the array
The Current Recruiter Array already has two userIds. The third one gets added successfully to the newList variable, but it doesn't get saved in the database. You can see the error below as it points to the third element that was just added in the controller
'Cast to [ObjectId] failed for value "[3]" at path "recruiters"'
The push() method adds one or more elements to the end of an array and returns the new length of the array.
You did:
const newList = position.recruiters.push(recruiter);
Then newList will be the new length of recruiters array(in your case is 3). You can fix this by changing your code to:
position.recruiters.push(recruiter);
position.markModified('recruiters'); // mark recruiters as having pending change
position.save();

Ignore null or empty values when saving a mongoose array schema type

I have the following schema:
const userSchema = new mongoose.Schema({
email: [{
type: String,
trim: true,
}]
})
When saving a new user,
const user = new User({
email: ["example#example.com", ""]
//or email: ["example#example.com", null]
})
try{
await user.save()
} catch (e) {
console.log(e)
}
This will save both those values (including empty string and null respectively).
Is there a way to save only the proper email value while discarding the empty or null value.
Instead of this:
"email" : [
"example#example.com",
""
],
store only the proper email:
"email" : [
"example#example.com",
],
Currently for other schema fields I am using set. For example, in the user schema above
url: {
type: String,
set: deleteEmpty
}
const deleteEmpty = (v) => {
if(!v) {
return undefined
}
return v
}
This will of course not save the url field at all if the value is empty or null.
Using this method on the email field above however will generate a null value.
Is there a way to store only the proper email value (i.e. "example#example.com" in this case while ignoring the null or empty value.)?
👨‍🏫 I think you can make it something like this code below 👇 with your userSchema:
userSchema.pre('save', function(next) {
this.email = this.email.filter(email => email);
next();
})
The code above ☝️ will igrone all empty or null value in an array. You can try it.
Or the Second Options, you can add required on your email field in your userSchema. It's well looks like this code below: 👇
const userSchema = new mongoose.Schema({
email: [{
type: String,
trim: true,
required: true // add some required
}],
});
💡 The code above ☝️, will give you an error if you passing an empty string on your array.
I hope it's can help you 🙏.
You can do the following to achieve what you want.
var arr = ['example#example.com', '']; // variable to keep the an array containing values
var i;
for (i = 0; i < arr.length; i++) {
if (arr[i] == null || arr[i] == '') {
arr.slice(i); // remove null or '' value
}
}
console.log('normalized array: ', arr);
// schema code
const user = new User({
email: arr
})
try{
await user.save()
} catch (e) {
console.log(e)
}
Good luck, I hope I answered your question.
If anyone has one or more fields that takes array value and wants to check for each field, I recommend using a middleware on the pre save hook.
supplierSchema.pre('save', normalizeArray)
const normalizeArrray = function(next) {
//take the list of object feilds and for each field check if it is array
Object.keys(this.toObject()).forEach((field) => {
if(Array.isArray(this[field])) {
//removes null or empty values
this[field] = this[field].filter(field => field)
}
})
next()
}
This just builds on the answer already approved above.
Simply set the default value of the field you wish to ignore, if empty, to undefined. Also, set required to false.
Use the code below:
const userSchema = new mongoose.Schema({
email: [{
type: String,
trim: true,
required: false,
default: undefined
}]
})

Is Mongoose support Set data type?

I wanna store unique values of any type, whether primitive values or object references. is possible? For example:
const activity = mongoose.Schema({
participants : {
type: Set, // <-- is possible?
required: true,
},
// ...
}
No, as of now it supports following data types in the latest version[5.6.3].
String,
Number,
Date,
Buffer,
Boolean,
Mixed,
ObjectId,
Array,
Decimal128,
Map
For more info on mongoose schema, please refer here
For data types supported by mongoDB, refer here
You can also use the $addToSet operator while updating that field.
For example:
const likeBook = async (req, res) => {
const {id: bookID} = req.params
const authorId = req.user.userId
const book = await Book.findByIdAndUpdate({_id: bookID}, {
$addToSet: {
like : authorId
}
})
if (!book) {
return new NotFoundError(`No Book with id: ${bookID}`)
}
await book.save()
res.status(StatusCodes.OK).json({book})
}
This will only add authorId to like array of Book if that Id doesn't exist.
You can use a setter to create a set-like array:
const activity = mongoose.Schema({
participants : {
type: Schema.Types.Mixed, // Array would probably also work
required: true,
set: toSet
},
// ...
}
function toSet(a) {
return [...new Set(a)];
}

TypeError: expected string but received array postman

I was trying to send form data that has multiple fields with the same name, I'm getting back "TypeError: expected string but received array".
I think the problem is with postman, I want to have multiple participant fields, and those would be added to the should be added to the array.
final results of array
// this is from models/Battle
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Create Schema
const BattleSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
date: {
type: Date,
default: Date.now
},
category: {
type: Number,
required: true // this will come from the selected category
},
winner: {
type: Number,
default: 0
},
status: {
type: Number,
default: 0 // 0 means the battle is closed, 1 means the battle is open for votes, the status will stay 0 until all participants dropped
},
participants: [
{
participant: {
type: Schema.Types.ObjectId,
required: true
}
}
]
});
module.exports = Battle = mongoose.model('battles', BattleSchema);
//this is from routes/api/battles
// #route POST api/battles
// #desc Create battle
// #access Private
router.post(
'/create-battle',
passport.authenticate('jwt', { session: false }),
(req, res) => {
const { errors, isValid } = validateBattleInput(req.body);
// Check Validation
if (!isValid) {
// If any errors, send 400 with errors object
return res.status(400).json(errors);
console.log(errors);
}
const newBattle = new Battle({
user: req.user.id,
category: req.body.category,
participant: req.body.participant
});
//save
newBattle.save().then(battle => {
// const participant = req.body.participant;
const participant = req.body.participant;
// add participants to array
battle.participants.push( participant );
console.log(typeof req.body.participant);
// get the inserted id
const battleId = battle._id;
res.json(battle);
});
}
);
// this is battle validation
const Validator = require('validator');
const isEmpty = require('./is-empty');
var bodyParser = require('body-parser');
module.exports = function validateBattleInput(data) {
let errors = {};
data.category = !isEmpty(data.category) ? data.category : '';
data.participant = !isEmpty(data.participant) ? data.participant : '';
if (Validator.isEmpty(data.category)) {
errors.category = 'Category field is required';
}
// if (Validator.isEmpty(data.challenger)) {
// errors.challenger = 'Challenger field is required';
// }
if (Validator.isEmpty(data.participant)) {
errors.participant = 'Participant field is required';
}
return {
errors,
isValid: isEmpty(errors)
};
};
TypeError: Expected string but received Array. ---throws an error in postman as well as in a terminal window. I suspect it could be the user schema definition mismatch
Please check your user model user schema eg
name: {
type: String,
required: true
}
it's receiving something else than expected.
try in your "body" tab, selecting "raw", and then to the right, select "JSON (application/json)" instead of "text".
I'm assuming your API endpoint uses JSON instead of a url-encoded form data, just because you are running an API using express and mongoose. but you should clarify that on the question if it isn't the case.
Write a proper JSON body, I mean, use double quotes for keys as in:
{"model": { "property": "value", "property2": 1}}
and try with the wrapping object {"model": <YOUR BODY HERE>} or without to see what works for you, as it's typical to wrap the object, but sometimes people don't use them. (seeing this in your code: req.body.participant makes me think you probably don't).
(PS: not related with the question, but personally prefer ARC or Insomnia for rest clients, as the interface for them is cleaner)
If you want data to be sent in participants array all the fields should be participants and not participant
try sending data through raw data and then selecting application/data for better formatting
When testing in postman - Just figured out Key value must match your validation function defined variables. It's better to be consistent across your development.

Mongoose save model with object field

I'm trying to save a mongoose model that has a schema with an object field. When I try to save I get the error below. What am I missing?
I suspect this might have something to do with how mongoose objects are not quite like standard javascript objects since they are altered. However, what confounds me is that I'm doing exactly the same thing in another section of my code with a schema that has a nested object field and that works.
What I've tried:
To test, I changed my schema to list resulttype and resultround as individual fields without nesting and when I did that it worked.
I also tried creating an object with key value pairs outside of my model and then pass that object to my model. That did not work either.
Schema
var ResultSchema = new mongoose.Schema({
event_id : mongoose.Schema.ObjectId,
event_name: String,
event_type: String,
resultdate : String,
resulttype: {
type: String,
round: Number
},
// resulttype: String,
// resultround: Number,
});
Model save:
var newResult = new ResultModel({
// objNewResult
event_id: req.body.eventid,//hidden field
event_name: req.body.eventname, //hidden field
resultdate: req.body.resultdate,
// resulttype: resulttypelist,
// resultround: resultroundlist,
resulttype: {
type: req.body.resulttypelist,
round: req.body.resultroundlist
}
});
newResult.save(function (err, result) {
if (err) {
console.log("SOMETHING WENT WRONG");
console.log(err);
} else {
console.log("SUCCESSFUL RESULT ADDITION");
}
});
Error:
ValidationError: results validation failed: resulttype: Cast to String failed for value "{ type: 'standard', round: '1' }" at path "resulttype"
type is a reserved keyword in Mongoose schemas. It's used to specify the type of the field. When you specify this:
resulttype: {
type: String,
round: String
},
Mongoose will consider the field resulttype to be a String. So you have to use another name than type.
type is a reserved key:
By default, if you have an object with key 'type' in your schema, mongoose will interpret it as a type declaration. Source.
So right now, resulttype is expected to be of type String. You can use a different key instead:
Schema:
resulttype: {
resultType: String,
round: String
},
Model Save:
var newResult = new ResultModel({
// ...
resulttype: {
resultType: req.body.resulttypelist,
round: parseFloat(req.body.resultroundlist),
}
});

Resources