adds validations to backend

This commit is contained in:
James
2020-04-19 14:18:34 -04:00
parent 45d8745f74
commit 6e39f39c6f
9 changed files with 148 additions and 39 deletions

View File

@@ -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"
} }

View File

@@ -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"

View File

@@ -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;

View 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;

View File

@@ -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,
}; };

View File

@@ -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: [

View File

@@ -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 = {};

View File

@@ -1,9 +1,39 @@
const executeFieldHooks = async (fields, data, hook) => { /* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
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) { for (const field of fields) {
if (typeof field.hooks[hook] === 'function') { if (field.required) {
const result = await field.hooks[hook](data); 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;

View File

@@ -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;