adds validations to backend
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"ignore": [
|
"ignore": [
|
||||||
".git",
|
".git",
|
||||||
|
"node_modules",
|
||||||
"node_modules/**/node_modules",
|
"node_modules/**/node_modules",
|
||||||
"src/client"
|
"src/client"
|
||||||
],
|
],
|
||||||
"watch": [
|
"watch": [
|
||||||
"src/"
|
"src/",
|
||||||
|
"demo/"
|
||||||
],
|
],
|
||||||
"ext": "js,json"
|
"ext": "js,json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,8 @@
|
|||||||
"test:int": "cross-env NODE_ENV=test jest --forceExit",
|
"test:int": "cross-env NODE_ENV=test jest --forceExit",
|
||||||
"cov": "npm run core:build && node ./node_modules/jest/bin/jest.js src/tests --coverage",
|
"cov": "npm run core:build && node ./node_modules/jest/bin/jest.js src/tests --coverage",
|
||||||
"dev": "nodemon demo/server.js",
|
"dev": "nodemon demo/server.js",
|
||||||
"server": "node demo/server.js",
|
|
||||||
"lint": "eslint **/*.js",
|
"lint": "eslint **/*.js",
|
||||||
"debug:test:int": "node --inspect-brk node_modules/.bin/jest --runInBand"
|
"debug:test:int": "node --inspect-brk node_modules/.bin/jest"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"payload": "./src/bin/index.js"
|
"payload": "./src/bin/index.js"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const executePolicy = require('../../users/executePolicy');
|
const executePolicy = require('../../users/executePolicy');
|
||||||
const executeFieldHooks = require('../../fields/executeHooks');
|
const executeFieldHooks = require('../../fields/executeHooks');
|
||||||
|
const validate = require('../../fields/validate');
|
||||||
|
|
||||||
const create = async (args) => {
|
const create = async (args) => {
|
||||||
try {
|
try {
|
||||||
@@ -9,8 +10,6 @@ const create = async (args) => {
|
|||||||
|
|
||||||
await executePolicy(args, args.config.policies.create);
|
await executePolicy(args, args.config.policies.create);
|
||||||
|
|
||||||
// Await validation here
|
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
Model: args.Model,
|
Model: args.Model,
|
||||||
config: args.config,
|
config: args.config,
|
||||||
@@ -22,13 +21,19 @@ const create = async (args) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// 2. Execute before create field-level hooks
|
// 2. Validate incoming data
|
||||||
|
// /////////////////////////////////////
|
||||||
|
|
||||||
|
await validate(args.config.fields, args.data);
|
||||||
|
|
||||||
|
// /////////////////////////////////////
|
||||||
|
// 3. Execute before create field-level hooks
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|
||||||
options.data = await executeFieldHooks(args.config.fields, args.data, 'beforeCreate');
|
options.data = await executeFieldHooks(args.config.fields, args.data, 'beforeCreate');
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// 3. Execute before collection hook
|
// 4. Execute before collection hook
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|
||||||
const { beforeCreate } = args.config.hooks;
|
const { beforeCreate } = args.config.hooks;
|
||||||
@@ -38,7 +43,7 @@ const create = async (args) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// 4. Perform database operation
|
// 5. Perform database operation
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -60,7 +65,7 @@ const create = async (args) => {
|
|||||||
result = result.toJSON({ virtuals: true });
|
result = result.toJSON({ virtuals: true });
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// 5. Execute after collection hook
|
// 6. Execute after collection hook
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|
||||||
const { afterCreate } = args.config.hooks.afterCreate;
|
const { afterCreate } = args.config.hooks.afterCreate;
|
||||||
@@ -70,7 +75,7 @@ const create = async (args) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// 6. Return results
|
// 7. Return results
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
10
src/errors/ValidationError.js
Normal file
10
src/errors/ValidationError.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const httpStatus = require('http-status');
|
||||||
|
const APIError = require('./APIError');
|
||||||
|
|
||||||
|
class ValidationError extends APIError {
|
||||||
|
constructor(results) {
|
||||||
|
super(results, httpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ValidationError;
|
||||||
@@ -5,6 +5,7 @@ const MissingCollectionLabel = require('./MissingCollectionLabel');
|
|||||||
const MissingGlobalLabel = require('./MissingGlobalLabel');
|
const MissingGlobalLabel = require('./MissingGlobalLabel');
|
||||||
const NotFound = require('./NotFound');
|
const NotFound = require('./NotFound');
|
||||||
const Forbidden = require('./Forbidden');
|
const Forbidden = require('./Forbidden');
|
||||||
|
const ValidationError = require('./ValidationError');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
APIError,
|
APIError,
|
||||||
@@ -14,4 +15,5 @@ module.exports = {
|
|||||||
MissingGlobalLabel,
|
MissingGlobalLabel,
|
||||||
NotFound,
|
NotFound,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
|
ValidationError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ const formatErrorResponse = (incoming) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(incoming.message)) {
|
||||||
|
return {
|
||||||
|
errors: incoming.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (incoming.name) {
|
if (incoming.name) {
|
||||||
return {
|
return {
|
||||||
errors: [
|
errors: [
|
||||||
@@ -21,9 +27,6 @@ const formatErrorResponse = (incoming) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the Mongoose error does not get returned with incoming && incoming.errors,
|
|
||||||
// it's of a type that we really don't know how to handle. Sometimes this means a TypeError,
|
|
||||||
// which we might be able to manipulate to get the error message itself and send that back.
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
errors: [
|
errors: [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const sanitizeFields = (fields) => {
|
|||||||
if (!field.type) throw new MissingFieldType(field);
|
if (!field.type) throw new MissingFieldType(field);
|
||||||
|
|
||||||
if (typeof field.validation === 'undefined') {
|
if (typeof field.validation === 'undefined') {
|
||||||
field.validation = validations[field.type];
|
field.validate = validations[field.type];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!field.hooks) field.hooks = {};
|
if (!field.hooks) field.hooks = {};
|
||||||
|
|||||||
@@ -1,9 +1,39 @@
|
|||||||
const executeFieldHooks = async (fields, data, hook) => {
|
/* eslint-disable no-await-in-loop */
|
||||||
for (const field of fields) {
|
/* eslint-disable no-restricted-syntax */
|
||||||
if (typeof field.hooks[hook] === 'function') {
|
|
||||||
const result = await field.hooks[hook](data);
|
const { ValidationError } = require('../errors');
|
||||||
|
|
||||||
|
const iterateFields = async (fields, data, errors, path = '') => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
await Promise.all(data.map(async (row, i) => {
|
||||||
|
await iterateFields(fields, row, errors, `${path}.${i}.`);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
for (const field of fields) {
|
||||||
|
if (field.required) {
|
||||||
|
const validationResult = await field.validate(data[field.name], field);
|
||||||
|
|
||||||
|
if (validationResult !== true) {
|
||||||
|
errors.push({
|
||||||
|
field: field.name,
|
||||||
|
message: validationResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.fields) {
|
||||||
|
await iterateFields(field.fields, data[field.name], errors, `${path}${field.name}.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = executeFieldHooks;
|
const validate = async (fields, data) => {
|
||||||
|
const errors = [];
|
||||||
|
await iterateFields(fields, data, errors);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new ValidationError(errors);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = validate;
|
||||||
|
|||||||
@@ -1,35 +1,93 @@
|
|||||||
const fieldToValidatorMap = {
|
const fieldToValidatorMap = {
|
||||||
number: (field, value) => {
|
number: (value, field) => {
|
||||||
const parsedValue = parseInt(value, 10);
|
const parsedValue = parseInt(value, 10);
|
||||||
|
|
||||||
if (typeof parsedValue !== 'number') return false;
|
if (typeof parsedValue !== 'number' || Number.isNaN(parsedValue)) {
|
||||||
if (field.max && parsedValue > field.max) return false;
|
return `${field.label} value is not a valid number.`;
|
||||||
if (field.min && parsedValue < field.min) return false;
|
}
|
||||||
|
|
||||||
|
if (field.max && parsedValue > field.max) {
|
||||||
|
return `${field.label} value is greater than the max allowed value of ${field.max}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.min && parsedValue < field.min) {
|
||||||
|
return `${field.label} value is less than the min allowed value of ${field.min}.`;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
text: (field, value) => {
|
text: (value, field) => {
|
||||||
if (field.maxLength && value.length > field.maxLength) return false;
|
if (field.maxLength && value.length > field.maxLength) {
|
||||||
if (field.minLength && value.length < field.minLength) return false;
|
return `${field.label} length is greater than the max allowed length of ${field.maxLength}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.minLength && value.length < field.minLength) {
|
||||||
|
return `${field.label} length is less than the minimum allowed length of ${field.minLength}.`;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
email: value => /\S+@\S+\.\S+/.test(value),
|
email: (value, field) => {
|
||||||
textarea: (field, value) => {
|
if (/\S+@\S+\.\S+/.test(value)) {
|
||||||
if (field.maxLength && value.length > field.maxLength) return false;
|
return true;
|
||||||
if (field.minLength && value.length < field.minLength) return false;
|
}
|
||||||
|
return `${field.label} is not a valid email address.`;
|
||||||
|
},
|
||||||
|
textarea: (value, field) => {
|
||||||
|
if (field.maxLength && value.length > field.maxLength) {
|
||||||
|
return `${field.label} length is greater than the max allowed length of ${field.maxLength}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.minLength && value.length < field.minLength) {
|
||||||
|
return `${field.label} length is less than the minimum allowed length of ${field.minLength}.`;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
wysiwyg: value => (!!value),
|
wysiwyg: (value, field) => {
|
||||||
code: value => (!!value),
|
if (value) return true;
|
||||||
checkbox: value => Boolean(value),
|
|
||||||
date: value => value instanceof Date,
|
return `${field.label} is required.`;
|
||||||
upload: value => (!!value),
|
},
|
||||||
relationship: value => (!!value),
|
code: (value, field) => {
|
||||||
repeater: value => (!!value),
|
if (value) return true;
|
||||||
select: value => (!!value),
|
|
||||||
flexible: value => (!!value),
|
return `${field.label} is required.`;
|
||||||
|
},
|
||||||
|
checkbox: (value, field) => {
|
||||||
|
if (value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${field.label} can only be equal to true or false.`;
|
||||||
|
},
|
||||||
|
date: (value, field) => {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${field.label} is not a valid date.`;
|
||||||
|
},
|
||||||
|
upload: (value, field) => {
|
||||||
|
if (value) return true;
|
||||||
|
return `${field.label} is required.`;
|
||||||
|
},
|
||||||
|
relationship: (value, field) => {
|
||||||
|
if (value) return true;
|
||||||
|
return `${field.label} is required.`;
|
||||||
|
},
|
||||||
|
repeater: (value, field) => {
|
||||||
|
if (value) return true;
|
||||||
|
return `${field.label} is required.`;
|
||||||
|
},
|
||||||
|
select: (value, field) => {
|
||||||
|
if (value) return true;
|
||||||
|
return `${field.label} is required.`;
|
||||||
|
},
|
||||||
|
flexible: (value, field) => {
|
||||||
|
if (value) return true;
|
||||||
|
return `${field.label} is required.`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = fieldToValidatorMap;
|
module.exports = fieldToValidatorMap;
|
||||||
|
|||||||
Reference in New Issue
Block a user