AJV validation in Nodejs - node.js

I am getting an error if the allRequired keyword is set. If it's not given there is no error. The documentation says this keyword must to be given to object types. When it's not given the validation passes even for wrong input (tested via Postman)
Here is the schema, it's exported from another file:
const ticketSchema = {
type: 'object',
properties: {
firstName: {
type: 'string',
minLength: 1,
},
lastName: {
type: 'string',
minLength: 1,
},
ticketId: {
type: 'string',
minLength: 6,
maxLength: 6,
},
},
allRequired: true, // <---- error occurs here
};
export default ticketSchema;
Error message:
Error: strict mode: unknown keyword: "allRequired"
Validation
const ajv = new Ajv();
const validateTicket = ajv.compile(ticketSchema);
const ticket = {
'first-name': firstName,
'last-name': lastName,
'route-id': routeID,
};
const valid = validateTicket(ticket);
if (!valid) {
res.status(422).send(validateTicket.errors);
return;
}

There is no allRequired property in JSON schema. I looked for it up here in specification. It is not there. So, which documentation do you refer to?
There is no allRequired property as far as I know.
But, if you want to make all the properties required, you need to specify a property called required. It is an array with required field names as elements. So, in your case, it would be:
const ticketSchema = {
type: 'object',
properties: {
firstName: {
type: 'string',
minLength: 1,
},
lastName: {
type: 'string',
minLength: 1,
},
ticketId: {
type: 'string',
minLength: 6,
maxLength: 6,
},
},
required: ['firstName', 'lastName', 'ticketId']
};
Update:
allRequired is not part of JSON specification. It is part of ajv-keywords module. You need to initialize it as follows:
const Ajv = require('ajv');
const ajv = new Ajv();
require('ajv-keywords')(ajv);
const validateTicket = ajv.compile(ticketSchema);
const ticket = {
'first-name': firstName,
'last-name': lastName,
'route-id': routeID,
};
const valid = validateTicket(ticket);
if (!valid) {
res.status(422).send(validateTicket.errors);
return;
}

Update
There were other mistakes too. The key names must be the same as given in the scheme, otherwise the validation won't work.
Example:
const scheme = {
type: 'object',
properties: {
firstName: {
type: 'string',
},
lastName: {
type: 'string',
},
},
};
// correct - key names are the same
data = {
firstName: 'Alex',
lastName: 'Smith',
};
// incorrect - key names aren't the same
data = {
'first-name': 'Alex',
'last-name': 'Smith',
};

Related

How to update deeply nested documents in mongoose v6.2.2

I am trying to update deeply nested documents and confusing myself with all of the nesting. Below is my model and code so far. I want to update 'purchased' value of inventory based on the size variable that is passed in. I was reading about arrayFilters but I still cannot figure it out.
model:
const mongoose = require('mongoose');
const inventorySchema = new mongoose.Schema({
size: {
type: String,
},
purchased: {
type: Number,
},
used: {
type: Number,
},
});
const kidsSchema = new mongoose.Schema({
firstName: {
type: String,
trim: true,
minlength: 1,
maxlength: 99,
},
currentChild: {
type: Boolean,
},
brandPreference: {
type: String,
trim: true,
minlength: 1,
maxlength: 99,
},
currentSize: {
type: String,
},
currentSizeLabel: {
type: String,
},
lowAlert: {
type: String,
},
diaperHistory: [diaperHistorySchema],
totalPurchased: {
type: Number,
},
totalUsed: {
type: Number,
},
inventory: [inventorySchema],
});
const KidsRecordSchema = new mongoose.Schema({
kids: [kidsSchema],
});
const KidsRecord = mongoose.model('KidsRecord', KidsRecordSchema);
exports.KidsRecord = KidsRecord;
code:
/**
* #description PUT add diapers to kids inventory
*/
router.put('/update/:size', auth, async (req, res) => {
let id = req.body.user_id;
let kidID = req.body.kids_id;
let size = req.params.size;
let purchased = req.body.purchased;
try {
let record = await KidsRecord.findOne({ user_id: id });
let subRecord = record.kids.id(kidID);
let inventory = subRecord.inventory.filter((x) => x.size == size);
console.log('inventory', inventory);
// inventory give me:
// [{ "size": "0", "purchased": 0, "used": 0, "_id": "625e91571be23abeadbfbee6"}]
// which is what I want to target but then how do I apply $set to it?
// $set... ??
if (!subRecord) {
res.send({ message: 'No kids for this user.' });
return;
}
res.send(inventory);
} catch (error) {
res.send({ message: error.message });
}
});
I can drill down and find the correct inventory object I want to update, but not sure how to actually change in and save.

findByIdAndUpdate overwrites existing value

facing a small issue that whenever I try to update a 'comments' property it automatically overwrites old property, doesn't add next value to the array. Tried many options like adding $set parameter as option, removing it, adding overwrite: false, but not successfully. Been looking at the docs, but feels like I'm something missing and even docs can't help me.
My Model:
const mongoose = require("mongoose");
const TicketSchema = new mongoose.Schema({
title: {
type: String,
minlength: 5,
maxlength: 50,
required: [true, "Please validate title"]
},
description: {
type: String,
minlength: 10,
maxlength: 200,
required: [true, "Please validate description"]
},
comments: {
type: Array,
default: []
},
status: {
type: String,
enum: ["in progress", "resolved", "pending"],
default: "pending",
},
priority: {
type: String,
enum: ["regular", "medium", "high"],
default: "regular"
},
createdBy: {
type: mongoose.Types.ObjectId,
ref: "User",
required: [true, "Please provide user"],
}
}, {
timestamps: true,
});
module.exports = mongoose.model('Ticket', TicketSchema);
My controller:
const updateUser = async (req, res) => {
const {
body: {username, email, firstname, lastname, country, password, comments},
params: {
id: employeeId
}
} = req;
const user = await User.findByIdAndUpdate(employeeId, req.body, {
$set: {
comments: req.body.comments
}
}, {
new: true, runValidators: true, upsert: true
}).select(["-password"]);
// Don't allow IT users to change credentials for IT and ADM users.
if (user.type.startsWith("IT") || user.type.startsWith("ADM")) throw new NotFoundError(`No user with id ${employeeId}`);
if (!user) throw new NotFoundError(`No user with id ${employeeId}`);
res.status(StatusCodes.OK).json({user});
};
You may want to use $push operator instead of $set.
Assuming req.body.comments is an array with comments, you could use $each to construct something like this:
const user = await User.findByIdAndUpdate(employeeId, req.body, {
$push: {
comments: { $each: req.body.comments }
}, {
new: true, runValidators: true, upsert: true
}
}).select(["-password"]);

How get user name from the userId to display in a post?

Hi everyone I am trying to add the user's name in the post that is being created by that user but I'm running into trouble.
This is the part of the post where the user's name should be displayed
<Link style={{ textDecoration: "none", color: "black" }}>
<h4
onClick={() => this.handleShowUserProfile(event.userId)}
className="host-name"
>
{getUser(event.userId).name}
</h4>
</Link>
This is where the user is being grabbed from the database
import http from "./httpService";
const apiEndPoint = "http://localhost:3100/api/users";
export function getUsers() {
return http.get(apiEndPoint);
}
export function getUser(userId) {
return http.get(apiEndPoint + "/" + userId);
}
in the backend this is how the user schema looks like
const jwt = require("jsonwebtoken");
const config = require("config");
const Joi = require("joi");
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 5,
maxlength: 50
},
bio: {
type: String,
required: true,
minlength: 200,
maxlength: 400
},
interests: {
type: Array
},
email: {
type: String,
required: true,
minlength: 5,
maxlength: 255,
unique: true
},
password: {
type: String,
required: true,
minlength: 5,
maxlength: 1024
},
isAdmin: Boolean
});
userSchema.methods.generateAuthToken = function() {
const token = jwt.sign(
{ _id: this._id, isAdmin: this.isAdmin },
config.get("jwtPrivateKey")
);
return token;
};
const User = mongoose.model("User", userSchema);
function validateUser(user) {
const schema = {
name: Joi.string()
.min(5)
.max(50)
.required(),
bio: Joi.string()
.required()
.min(200)
.max(400),
interests: Joi.array().required(),
email: Joi.string()
.min(5)
.max(255)
.required()
.email(),
password: Joi.string()
.min(5)
.max(255)
.required()
};
return Joi.validate(user, schema);
}
module.exports.User = User;
module.exports.validate = validateUser;
this is the event schema...
const Joi = require("joi");
const mongoose = require("mongoose");
const { categorySchema } = require("./category");
const { userSchema } = require("./user");
const Event = mongoose.model(
"Events",
new mongoose.Schema({
image: {
type: String
},
title: {
type: String,
required: true,
minlength: 5,
maxlength: 50,
trim: true
},
user: {
type: userSchema,
required: true
},
details: {
type: String,
required: true,
minlength: 200,
maxlength: 300,
trim: true
},
category: {
type: categorySchema,
required: true
},
numOfAttendies: {
type: Number,
required: true,
min: 3,
max: 10000
}
})
);
this is the handleShowUserProfile
handleShowUserProfile = id => {
this.setState({
showUserProfile: true,
shownUserID: id,
user: getUser(id)
});
};
handleHideUserProfile = () => {
this.setState({
showUserProfile: false
});
};
If I had to guess, getUser() likely returns a promise, not a value. You'll need to look into reading the name from props(state) and updating state instead of trying to do it all right there.
Try logging out that get request. See if the results it returns match up with what you're trying to get from it.
export function getUser(userId) {
const results = http.get(apiEndPoint + "/" + userId);
console.log("results of getUser ::: ", results)
return results
}
The getUser function is an asynchronous function so it will return a promise in most cases you need to use state and the lifecycle methods to fetch and set the username.
Can you update your handleShowUserProfile method like the following?
handleShowUserProfile = async (id) => {
const user = await getUser(id);
this.setState({
showUserProfile: true,
shownUserID: id,
user: user
});
};
I assume your api returns data for http://localhost:3100/api/users/:userId endpoint.
After you check your api and sure that it returns data, you need to check if you send a valid userId in this line.
onClick={() => this.handleShowUserProfile(event.userId)}
Lastly you should also change the following line since the user data is in the state.
{getUser(event.userId).name} => {this.state.user && this.state.user.name }

Mongoose: Add validators on the fly on some parameters depending on queries

I am new to Mongoose and would like to know if it is possible to add validators on the fly on some parameters depending on queries. I have for example a schema like below:
var user = new Schema({
name: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true },
city: { type: String },
country: { type: String }
});
For a simple registration i force users giving the name, the email and the password. The Schema on top is OK. Now later I would like to force users giving the city and the country. Is it possible for example to update a user's document with the parameters city and country on required? I am avoiding to duplicate user schema like below:
var userUpdate = new Schema({
name: { type: String },
email: { type: String },
password: { type: String },
city: { type: String, required: true },
country: { type: String, required: true }
});
What you would need to do in this case is have one Schema and make your required a function which allows null and String:
var user = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
city: {
type: String,
required: function() {
return typeof this.city === 'undefined' || (this.city != null && typeof this.city != 'string')
}
}
});
You can extract this and make it an outside function which then you can use for county etc.
What this does is it makes the field required but also you can set null to it. In this way you can have it null in the beginning and then set it later on.
Here is the doc on required.
As far as I know, no, it is not possible.
Mongoose schema are set on collection, not on document.
you could have 2 mongoose model pointing to the same collection with different Schema, but it would effectively require to have duplicated Schema.
personnally, in your situation, I would create a single home-made schema like data structure and a function who, when feeded with the data structure, create the two version of the Schema.
by example :
const schemaStruct = {
base : {
name: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true },
city: { type: String },
country: { type: String }
}
addRequired : ["city", "country"]
}
function SchemaCreator(schemaStruct) {
const user = new Schema(schemaStruct.base)
const schemaCopy = Object.assign({}, schemaStruct.base)
schemaStruct.addRequired.forEach(key => {
schemaCopy[key].required = true;
})
const updateUser = new Schema(schemaCopy);
return [user, updateUser];
}

NodeJS: Server-side request data validation

What is the right way to validate incoming data on server side?
I'm using lodash for simple validation like isObject or isArray etc, and validator for cases when i need to validate, say, if a string isEmail. But all this looks awkward and i'm not sure if this gonna hurt performance a lot or not so much.
There should be a way to validate incoming data the more elegant way.
One way to do it would be to use schema-inspector.
It's a module meant to validate json objects based on a json-schema description.
Here is an example from the github README :
var inspector = require('schema-inspector');
// Data that we want to sanitize and validate
var data = {
firstname: 'sterling ',
lastname: ' archer',
jobs: 'Special agent, cocaine Dealer',
email: 'NEVER!',
};
// Sanitization Schema
var sanitization = {
type: 'object',
properties: {
firstname: { type: 'string', rules: ['trim', 'title'] },
lastname: { type: 'string', rules: ['trim', 'title'] },
jobs: {
type: 'array',
splitWith: ',',
items: { type: 'string', rules: ['trim', 'title'] }
},
email: { type: 'string', rules: ['trim', 'lower'] }
}
};
// Let's update the data
inspector.sanitize(sanitization, data);
/*
data is now:
{
firstname: 'Sterling',
lastname: 'Archer',
jobs: ['Special Agent', 'Cocaine Dealer'],
email: 'never!'
}
*/
// Validation schema
var validation = {
type: 'object',
properties: {
firstname: { type: 'string', minLength: 1 },
lastname: { type: 'string', minLength: 1 },
jobs: {
type: 'array',
items: { type: 'string', minLength: 1 }
},
email: { type: 'string', pattern: 'email' }
}
};
var result = inspector.validate(validation, data);
if (!result.valid)
console.log(result.format());
/*
Property #.email: must match [email], but is equal to "never!"
*/
The sanitization schema is meant to "clean" your json before validating it (Setting optional values, trying to convert numbers to string, etc).
The validation schema describes the properties your json should respect.
You then call inspector.validate to check if everything is fine.

Resources