Joi validate if the number is a multiple of x - node.js

Current validation:
const myPacket = Joi.object({
cost: Joi.number().precision(2).required()
})
Here, how do I check if my cost field is a multiple of 0.25 or not? So, for example, valid values for cost would be 1.25, 2.5, 2.25, etc.

You can use custom method and write your own logic there https://github.com/sideway/joi/blob/master/API.md#anycustommethod-description
example
const Joi = require('joi')
const myCustomValidation = (value, helpers) => {
if (typeof value !== 'number') return helpers.error('any.invalid')
if (parseFloat(value*4) % 1 === 0) {
// your logic above
return value
} else return helpers.error('any.invalid')
}
const schema = Joi.object({
customField: Joi.number().custom(myCustomValidation, 'my custom validation description')
})
schema.validate({customField: 1.25})
schema.validate({customField: 1.251})

Related

how to ignore null or blank value while writing to JSON using NodeJS

I'm writing below content to JSON file, I would like to ignore the fields which has value null or blank - in this case I want to ignore productPrice and productRating fields while writing to JSON file, I'm not much familiar with NodeJS - can someone please help how can I achieve this in NodeJS?
Please find my code below:
const fs = require('fs');
const params = {
productID: 'prd323434',
productName: 'Google',
productDesc: 'Larum ipsum',
productPrice: null,
productRating: '',
productReview: 'Lorum ipsum'
};
var data = {
productID: params.productID,
productName: params.productName,
productDesc: params.productDesc,
productPrice: params.productPrice,
productRating: params.productRating,
productReview: params.productReview
};
let jsonContent = JSON.stringify(data);
fs.writeFileSync('test.json', jsonContent);
console.log(jsonContent)
You look like you are just filtering your object for "falsy" values.
So take params, use Object.entries on it to get something like this:
[
[productID, 'prd323434'],
[productName, 'Google'],
...
]
Use the filter method on the result, destructure each param with ([k,v]). Then only return it if v is "truthy".
const data = Object.fromEntries(Object.entries(params).filter(([k,v]) => v))
Or perhaps in a more readable way:
const entries = Object.entries(params)
const filtered = entries.filter(([k,v]) => {
if (v === null || v === "") {
return false
} else {
return true
}
}))
const data = Object.fromEntries(filtered)

Body parameter in ArangoDB

I wrote the following router in Foxx Microservices:
router.post('/PersonInformation', function (req, res) {
const keys = db._query(aql`
FOR p IN Personal
FILTER (p.stateId IN ${req.queryParams.statePar} OR IS_NULL(${req.queryParams.statePar}))
AND p.empDate >= TO_NUMBER(${req.queryParams.fromDatePar}) AND p.empDate <= TO_NUMBER(${req.queryParams.toDatePar})
RETURN p
`);
res.send(keys);
})
.queryParam('statePar', joi.array().default(null), 'State Parameter')
.queryParam ('fromDatePar', joi.string().required(), 'FromDate Parameter')
.queryParam('toDatePar', joi.string().required(), 'ToDate Parameter')
.response(joi.array().items(
joi.string().required()
).required(), 'List of entry keys.')
.summary('List entry keys')
.description('Assembles a list of keys of entries in the collection.');
How i can convert queryParam to body parameter. I used .body instead of .queryParam, but it does not answer. I also wrote the table as follows, but it still does not work:
router.post('/PersonInformation', function (req, res) {
const distributorIdPar = req.body;
const cityPar = req.body;
const productIdPar = req.body;
const fromDatePar = req.body;
const toDatePar = req.body;
const keys = db._query(aql`
const keys = db._query(aql`
FOR p IN Personal
FILTER (p.stateId IN ${req.body.statePar} OR IS_NULL(${req.body.statePar}))
AND p.empDate >= TO_NUMBER(${req.body.fromDatePar}) AND p.empDate <= TO_NUMBER(${req.body.toDatePar})
RETURN p
`);
res.send(keys);
})
.response(joi.array().items(
joi.string().required()
).required(), 'List of entry keys.')
.summary('List entry keys')
.description('Assembles a list of keys of entries in the collection.');
You have a duplicate line const keys = db._query(aql` that breaks the syntax. Furthermore, you are not setting .body(joi.object()) or .body(['application/json']), which leads to req.body being a Buffer:
If no body has been defined for the current route, the value will be identical to rawBody – docs
You also assign req.body to various variables but don't actually use them.
I would assign the body parameters to local variables using destructuring for the guaranteed values, and use a regular assignment for statePar so that it can default to null if it is omitted from the body. Furthermore, I would enforce numeric values for fromDatePar and toDatePar (note that this requires strict() in joi or it will happily accept number strings) and remove TO_NUMBER() from the query. Below code accepts either an array of strings for statePar or expects the attribute to not be present. It disallows "statePar":null, "statePar":[], and "statePar":[null], but you could change that if desired.
'use strict';
const joi = require('joi');
const { db, aql } = require('#arangodb');
const createRouter = require('#arangodb/foxx/router');
const router = createRouter();
router.post('/PersonInformation', function (req, res) {
const statePar = req.body.statePar || null;
const { fromDatePar, toDatePar } = req.body;
const keys = db._query(aql`
FOR p IN Personal
FILTER (p.stateId IN ${statePar} OR IS_NULL(${statePar}))
AND p.empDate >= ${fromDatePar} AND p.empDate <= ${toDatePar}
RETURN p
`);
res.send(keys);
})
.body(joi.object({
statePar: joi.array().items(
joi.string().required()
),
fromDatePar: joi.number().required(),
toDatePar: joi.number().required(),
}).required().strict(), 'Search parameters')
.response(joi.array().items(
joi.string().required()
).required(), 'List of entry keys.')
.summary('List entry keys')
.description('Assembles a list of keys of entries in the collection.');
module.context.use(router);

How to use MongoDB $ne on nested object property

I have a node API which connects to a mongoDB through mongoose. I am creating an advanced results middleware that enabled selecting, filtering, sorting, pagination etc. based on a Brad Traversy course Node.js API Masterclass With Express & MongoDB. This is all good.
I am adapting the code from the course to be able to use the $ne (not equal) operator and I want to be able to get a model that is not equal to a nested property (user id) of the model. I am using this for an explore feature to see a list of things, but I don't want to show the user their own things. I am having trouble figuring out how to access the id property.
********************* UPDATE *********************
It seems all the documentation I've read recommends writing const injected like this:
const injected = {
'user._id': { "$ne": req.user.id }
};
but for some reason it is not working. I can query top level properties that are just a plain string value like this:
const injected = {
access: { "$ne": "public" }
};
but not a property on an object. Does anyone know why? Is it because the property I want to query is an id? I've also tried:
const injected = {
'user._id': { "$ne": mongoose.Types.ObjectId(req.user.id) }
};
which also does not work...
So the model looks like this:
{
name: 'Awesome post',
access: 'public',
user: {
_id: '2425635463456241345', // property I want to access
}
}
then the actual advanced results middleware looks like this and it's the 'injected' object where I am trying to access id. In the course brad uses this syntax to use lte (/?averageCost[lte]=10000) but I do not get any results with my ne. Can anyone help me here?
const advancedResults = (model, populate) => async (req, res, next) => {
let query;
const injected = {
access: 'public',
'user._id[ne]': req.user.id, // I don't think user._id[ne] is correct
};
}
// Copy req.query
const reqQuery = { ...req.query, ...injected };
console.log('injected: ', injected);
// Fields to exclude
const removeFields = ['select', 'sort', 'page', 'limit'];
// Loop over removeFields and delete them from reqQuery
removeFields.forEach(param => delete reqQuery[param]);
// Create query string
let queryStr = JSON.stringify(reqQuery);
// Create operators ($gt, $gte, etc)
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in|ne)\b/g, match => `$${match}`);
// Finding resource and remove version
query = model.find(JSON.parse(queryStr)).select('-__v');
// Select Fields
if (req.query.select) {
const fields = req.query.select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (req.query.sort) {
const sortBy = req.query.sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
// Pagination
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 25;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const total = await model.countDocuments(JSON.parse(queryStr));
query = query.skip(startIndex).limit(limit);
if (populate) {
query = query.populate(populate);
}
// Executing query
const results = await query;
// Pagination result
const pagination = {};
if (endIndex < total) {
pagination.next = {
page: page + 1,
limit,
};
}
if (startIndex > 0) {
pagination.prev = {
page: page - 1,
limit,
};
}
res.advancedResults = {
success: true,
count: results.length,
pagination,
data: results,
};
next();
};
module.exports = advancedResults;
Answering your question about how to use $ne:
The use of $ne is as follows:
"field":{
"$ne": yourValue
}
Into your query should be like:
"user._id": {
"$ne": req.user.id
}
Example here
$ne operator will return all document where the field value don't match with the given value.
As you have done, to acces the nested field is necessary use the dot notation.
Also, to ensure it works, if your schema defines _id as ObjectId maybe is necessary parse req.user.id to ObjectId.
But if in your schema is a string then should works.
So try (not tested at all):
const injected = {
'user._id': { "$ne": req.user.id }
};

Is it possible to add an additional piece of logic to a "#hapi/joi" schema object?

const Joi = require('#hapi/joi');
function validateInput(input)({
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
numApples: Joi.number().min(1).max(5).required(),
numOranges: Joi.number().min(1).max(5).required()
});
return schema.validate(input);
}
What I'm trying to do is throw an error if the total apples and oranges doesn't equal 3 or more.
Is it possible to add custom logic this way using #hapi/joi?
You could define a new value numApplesAndOranges and set it equal to numApples + numOranges before including it in your schema.
You can now test like so: numApplesAndOranges: Joi.number().min(3).required().
Full code would look like:
const Joi = require('#hapi/joi');
function validateInput(input)({
const numApplesAndOranges = input.numApples + input.numOranges;
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
numApples: Joi.number().min(1).max(5).required(),
numOranges: Joi.number().min(1).max(5).required(),
numApplesAndOranges: Joi.number().min(3).required()
});
return schema.validate({...input, numApplesAndOranges});
}
You can do something similar to below:
const Joi = require('#hapi/joi');
function validateInput(input) {
// custom validation function by hapi/joi
const sum = function(fields = [], maxValue) {
return function(value, helpers) {
const sum = 0
fields.forEach(f => {
sum = sum + +input[f];
});
if (sum > maxValue) {
return helpers.error('any.invalid');
}
}
}
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
numApples: Joi.number().min(1).max(5).required(),
numOranges: Joi.number().min(1).max(5).required().custom(sum(['numApples', 'numOranges'], 3))
});
return schema.validate(input);
You can learn more about custom validation in Joi here.
Hope this helps!

Writing test for Joi validation

I have the following model:
const mongoose = require("mongoose");
const Joi = require("#hapi/joi");
const activitySchema = new mongoose.Schema({
title: {
type: String,
maxlength: 255,
minlength: 3,
required: true
}
});
const Activity = mongoose.model("Activity", activitySchema);
function validateActivity(activity) {
const schema = Joi.object({
title: Joi.string().min(3).max(255).required()
});
return schema.validate(activity)
}
module.exports.Activity = Activity;
module.exports.validate = validateActivity;
And I'm writing a unit test for the validateActivity function. I am not sure about what's the best practice for writing these tests.
So far I came up with:
const {validateActivity} = require("../../models/activity");
describe("activity model", () => {
let mockActivity;
beforeEach(() => {
mockActivity = {
title: "123"
}
});
it("should return an error if no title is provided", () => {
delete mockActivity.title;
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/any.required/);
});
it("should return an error if title is not a string", () => {
mockActivity.title = { test: "test" };
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.base/);
});
it("should return an error if title is less than 3 chars", () => {
mockActivity.title = "12";
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.min/);
});
it("should return an error if title is more than 255 chars", () => {
mockActivity.title = Array(258).join("a");
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.max/);
});
});
So once I add several fields to my model the testing will be quite repetitive. Is it necessary to write a test for each scenario?
I guess it all depends on how strict and important you want your validation to be. If you have a data centric application where your bread and butter is data. if you don't test your data thoroughly you might end up in a bad situation.
Now imagine in your case you have a schema amd you don't have tests. If you for example remove max(255) condition, your all tests will pass (since you don't have any) and your customers will be able to insert data which is longer than 255 which obviously will be wrong.
I don't see any thing wrong in what you are doing. You can consider grouping stuff when you have multiple fields. In nutshell, you should test your schema thoroughly if data integrity is super important for you.
After viewing this comment https://stackoverflow.com/a/33588497/5733078 it seems that it would not make sense to check every scenario since we can trust that the validate function has already been tested within Joi.

Resources