flattens validations on backend, ensures validations still run even if field is not required

This commit is contained in:
James
2020-06-16 17:42:00 -04:00
parent df31f4f144
commit 731c640e32
25 changed files with 285 additions and 209 deletions

View File

@@ -1,4 +1,4 @@
const validations = require('../fields/validations');
const validations = require('../validation/validations');
module.exports = [
{

View File

@@ -1,7 +1,7 @@
const passport = require('passport');
const executePolicy = require('../executePolicy');
const executeFieldHooks = require('../../fields/executeHooks');
const { validateCreate } = require('../../fields/validateCreate');
const validateCreate = require('../../validation/validateCreate');
const register = async (args) => {
try {

View File

@@ -5,7 +5,7 @@ import Label from '../../Label';
import Button from '../../../elements/Button';
import CopyToClipboard from '../../../elements/CopyToClipboard';
import generateAPIKey from './generateAPIKey';
import { text } from '../../../../../fields/validations';
import { text } from '../../../../../validation/validations';
import useForm from '../../Form/useForm';
import './index.scss';

View File

@@ -1,9 +1,9 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
import Error from '../../Error';
import { checkbox } from '../../../../../fields/validations';
import { checkbox } from '../../../../../validation/validations';
import Check from '../../../icons/Check';
import './index.scss';
@@ -28,6 +28,11 @@ const Checkbox = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const {
value,
showError,
@@ -38,7 +43,7 @@ const Checkbox = (props) => {
required,
initialData,
defaultValue,
validate,
validate: memoizedValidate,
disableFormData,
});

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import DatePicker from '../../../elements/DatePicker';
@@ -6,7 +6,7 @@ import withCondition from '../../withCondition';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import { date } from '../../../../../fields/validations';
import { date } from '../../../../../validation/validations';
import './index.scss';
@@ -31,24 +31,27 @@ const DateTime = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const {
value,
showError,
setValue,
formProcessing,
} = useFieldType({
path,
required,
initialData,
defaultValue,
validate,
validate: memoizedValidate,
});
const classes = [
'field-type',
baseClass,
showError && `${baseClass}--has-error`,
formProcessing && 'processing',
readOnly && 'read-only',
].filter(Boolean).join(' ');
@@ -72,7 +75,7 @@ const DateTime = (props) => {
<div className={`${baseClass}__input-wrapper`}>
<DatePicker
{...props}
onChange={readOnly || formProcessing ? setValue : undefined}
onChange={readOnly ? setValue : undefined}
value={value}
/>
</div>
@@ -90,6 +93,7 @@ DateTime.defaultProps = {
width: undefined,
style: {},
path: '',
readOnly: false,
};
DateTime.propTypes = {
@@ -103,6 +107,7 @@ DateTime.propTypes = {
errorMessage: PropTypes.string,
width: PropTypes.string,
style: PropTypes.shape({}),
readOnly: PropTypes.bool,
};
export default withCondition(DateTime);

View File

@@ -1,10 +1,10 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import withCondition from '../../withCondition';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import { email } from '../../../../../fields/validations';
import { email } from '../../../../../validation/validations';
import './index.scss';
@@ -26,6 +26,11 @@ const Email = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const {
value,
showError,
@@ -36,7 +41,7 @@ const Email = (props) => {
required,
initialData,
defaultValue,
validate,
validate: memoizedValidate,
enableDebouncedValue: true,
});

View File

@@ -16,7 +16,7 @@ import { useRenderedFields } from '../../RenderFields';
import { useLocale } from '../../../utilities/Locale';
import Error from '../../Error';
import useFieldType from '../../useFieldType';
import { flexible } from '../../../../../fields/validations';
import { flexible } from '../../../../../validation/validations';
import './index.scss';
@@ -44,11 +44,11 @@ const Flexible = (props) => {
const validationResult = validate(
value,
{
minRows, maxRows, singularLabel, blocks,
minRows, maxRows, singularLabel, blocks, required,
},
);
return validationResult;
}, [validate, maxRows, minRows, singularLabel, blocks]);
}, [validate, maxRows, minRows, singularLabel, blocks, required]);
const {
showError,

View File

@@ -4,7 +4,7 @@ import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import withCondition from '../../withCondition';
import { number } from '../../../../../fields/validations';
import { number } from '../../../../../validation/validations';
import './index.scss';

View File

@@ -1,10 +1,10 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import withCondition from '../../withCondition';
import { password } from '../../../../../fields/validations';
import { password } from '../../../../../validation/validations';
import './index.scss';
@@ -24,6 +24,11 @@ const Password = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const {
value,
showError,
@@ -35,7 +40,7 @@ const Password = (props) => {
required,
initialData,
defaultValue,
validate,
validate: memoizedValidate,
enableDebouncedValue: true,
});

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import useFieldType from '../../useFieldType';
@@ -6,7 +6,7 @@ import withCondition from '../../withCondition';
import Error from '../../Error';
import Label from '../../Label';
import RadioInput from './RadioInput';
import { radio } from '../../../../../fields/validations';
import { radio } from '../../../../../validation/validations';
import './index.scss';
@@ -27,6 +27,11 @@ const RadioGroup = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const {
value,
showError,
@@ -37,7 +42,7 @@ const RadioGroup = (props) => {
required,
initialData,
defaultValue,
validate,
validate: memoizedValidate,
});
const classes = [

View File

@@ -1,4 +1,6 @@
import React, { Component, useState, useEffect } from 'react';
import React, {
Component, useState, useEffect, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import Cookies from 'universal-cookie';
import some from 'async-some';
@@ -8,7 +10,7 @@ import ReactSelect from '../../../elements/ReactSelect';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import { relationship } from '../../../../../fields/validations';
import { relationship } from '../../../../../validation/validations';
import './index.scss';
@@ -304,12 +306,18 @@ const RelationshipFieldType = (props) => {
const hasMultipleRelations = Array.isArray(relationTo);
const dataToInitialize = initialData || defaultValue;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const fieldType = useFieldType({
...props,
path: path || name,
initialData: formattedInitialData,
defaultValue,
validate,
validate: memoizedValidate,
required,
});

View File

@@ -14,7 +14,7 @@ import { useRenderedFields } from '../../RenderFields';
import useFieldType from '../../useFieldType';
import { useLocale } from '../../../utilities/Locale';
import Error from '../../Error';
import { repeater } from '../../../../../fields/validations';
import { repeater } from '../../../../../validation/validations';
import './index.scss';
@@ -48,9 +48,9 @@ const Repeater = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { minRows, maxRows });
const validationResult = validate(value, { minRows, maxRows, required });
return validationResult;
}, [validate, maxRows, minRows]);
}, [validate, maxRows, minRows, required]);
const {
showError,

View File

@@ -1,11 +1,11 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import withCondition from '../../withCondition';
import ReactSelect from '../../../elements/ReactSelect';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import { select } from '../../../../../fields/validations';
import { select } from '../../../../../validation/validations';
import './index.scss';
@@ -78,6 +78,11 @@ const Select = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
}, [validate, required]);
const {
value,
showError,
@@ -89,7 +94,7 @@ const Select = (props) => {
required,
initialData,
defaultValue,
validate,
validate: memoizedValidate,
});
const classes = [

View File

@@ -4,7 +4,7 @@ import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
import { text } from '../../../../../fields/validations';
import { text } from '../../../../../validation/validations';
import './index.scss';
@@ -28,9 +28,9 @@ const Text = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { minLength, maxLength });
const validationResult = validate(value, { minLength, maxLength, required });
return validationResult;
}, [validate, maxLength, minLength]);
}, [validate, maxLength, minLength, required]);
const fieldType = useFieldType({
path,

View File

@@ -1,10 +1,10 @@
import React from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import useFieldType from '../../useFieldType';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
import { textarea } from '../../../../../fields/validations';
import { textarea } from '../../../../../validation/validations';
import './index.scss';
@@ -21,10 +21,17 @@ const Textarea = (props) => {
label,
placeholder,
readOnly,
minLength,
maxLength,
} = props;
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { minLength, maxLength, required });
return validationResult;
}, [validate, maxLength, minLength, required]);
const {
value,
showError,
@@ -35,7 +42,7 @@ const Textarea = (props) => {
required,
initialData,
defaultValue,
validate,
validate: memoizedValidate,
enableDebouncedValue: true,
});
@@ -86,6 +93,8 @@ Textarea.defaultProps = {
placeholder: null,
path: '',
readOnly: false,
minLength: undefined,
maxLength: undefined,
};
Textarea.propTypes = {
@@ -100,6 +109,8 @@ Textarea.propTypes = {
label: PropTypes.string,
placeholder: PropTypes.string,
readOnly: PropTypes.bool,
minLength: PropTypes.number,
maxLength: PropTypes.number,
};
export default withCondition(Textarea);

View File

@@ -53,7 +53,7 @@ const useFieldType = (options) => {
const sendField = useCallback(async (valueToSend) => {
const fieldToDispatch = { path, value: valueToSend };
fieldToDispatch.valid = (required && typeof validate === 'function') ? await validate(valueToSend || '') : true;
fieldToDispatch.valid = typeof validate === 'function' ? await validate(valueToSend || '') : true;
if (typeof fieldToDispatch.valid === 'string') {
fieldToDispatch.errorMessage = fieldToDispatch.valid;
@@ -65,7 +65,7 @@ const useFieldType = (options) => {
}
dispatchFields(fieldToDispatch);
}, [path, required, dispatchFields, validate, disableFormData]);
}, [path, dispatchFields, validate, disableFormData]);
// Method to return from `useFieldType`, used to

View File

@@ -2,7 +2,7 @@ const mkdirp = require('mkdirp');
const executePolicy = require('../../auth/executePolicy');
const executeFieldHooks = require('../../fields/executeHooks');
const { validateCreate } = require('../../fields/validateCreate');
const validateCreate = require('../../validation/validateCreate');
const { MissingFile } = require('../../errors');
const resizeAndSave = require('../../uploads/imageResizer');

View File

@@ -1,7 +1,7 @@
const executePolicy = require('../../auth/executePolicy');
const executeFieldHooks = require('../../fields/executeHooks');
const { NotFound, Forbidden } = require('../../errors');
const validate = require('../../fields/validateUpdate');
const validate = require('../../validation/validateUpdate');
const resizeAndSave = require('../../uploads/imageResizer');

View File

@@ -1,5 +1,5 @@
const { MissingFieldType } = require('../errors');
const validations = require('./validations');
const validations = require('../validation/validations');
const sanitizeFields = (fields) => {
if (!fields) return [];

View File

@@ -1,80 +0,0 @@
const { ValidationError } = require('../errors');
const createValidationPromise = async (data, field) => {
const result = await field.validate(data, field);
return { result, field };
};
exports.iterateFields = async (data, fields, path = '') => {
const validationPromises = [];
fields.forEach((field) => {
const dataToValidate = data || {};
if ((field.required && !field.localized && !field.condition)) {
// If this field does not have a name, it is for
// admin panel composition only and should not be
// validated against directly
if (field.name === undefined && field.fields) {
field.fields.forEach((subField) => {
validationPromises.push(createValidationPromise(dataToValidate[subField.name], subField));
});
} else if (field.fields) {
if (field.type === 'repeater' || field.type === 'flexible') {
const rowCount = Array.isArray(dataToValidate[field.name]) ? dataToValidate[field.name].length : 0;
validationPromises.push(createValidationPromise(rowCount, field));
if (Array.isArray(dataToValidate[field.name])) {
dataToValidate[field.name].forEach((rowData, i) => {
validationPromises.push(exports.iterateFields(rowData, field.fields, `${path}${field.name}.${i}.`));
});
}
} else {
validationPromises.push(exports.iterateFields(dataToValidate[field.name], field.fields, `${path}${field.name}`));
}
} else {
validationPromises.push(createValidationPromise(dataToValidate[field.name], field));
}
}
});
const validationResults = await Promise.all(validationPromises);
const errors = validationResults.reduce((results, result) => {
const { field, result: validationResult } = result;
if (Array.isArray(result)) {
return [
...results,
...result,
];
}
if (validationResult === false || typeof validationResult === 'string') {
const fieldPath = `${path}${field.name}`;
return [
...results,
{
field: fieldPath,
message: validationResult,
},
];
}
return results;
}, []);
return errors;
};
exports.validateCreate = async (data, fields) => {
try {
const errors = await exports.iterateFields(data, fields);
if (errors.length > 0) {
throw new ValidationError(errors);
}
} catch (error) {
throw error;
}
};

View File

@@ -1,46 +0,0 @@
const { ValidationError } = require('../errors');
const validateUpdate = async (data, fields) => {
const validationPromises = [];
const validatedFields = [];
Object.keys(data).forEach((key) => {
const dataToValidate = data[key];
const field = fields.find(matchedField => matchedField.name === key);
if (field && dataToValidate !== undefined) {
validationPromises.push(field.validate(dataToValidate, field));
validatedFields.push(field);
}
});
const validationResults = await Promise.all(validationPromises);
const errors = validationResults.reduce((results, result, i) => {
const field = validatedFields[i];
if (Array.isArray(result)) {
return [
...results,
...result,
];
} if (result !== true) {
return [
...results,
{
field: field.name,
message: result,
},
];
}
return results;
}, []);
if (errors.length > 0) {
throw new ValidationError(errors);
}
};
module.exports = validateUpdate;

View File

@@ -0,0 +1,31 @@
exports.createValidationPromise = async (data, field) => {
const result = await field.validate(data, field);
return { result, field };
};
exports.getErrorResults = async (resultPromises, path) => {
const results = await Promise.all(resultPromises);
return results.reduce((formattedResults, result) => {
const { field, result: validationResult } = result;
if (Array.isArray(result)) {
return [
...formattedResults,
...result,
];
}
if (validationResult === false || typeof validationResult === 'string') {
return [
...formattedResults,
{
field: `${path}${field.name}`,
message: validationResult,
},
];
}
return formattedResults;
}, []);
};

View File

@@ -0,0 +1,51 @@
const { ValidationError } = require('../errors');
const { createValidationPromise, getErrorResults } = require('./utilities');
const iterateFields = async (data, fields, path = '') => {
const validationPromises = [];
fields.forEach((field) => {
const dataToValidate = data || {};
if (!field.condition) {
// If this field does not have a name, it is for
// admin panel composition only and should not be
// validated against directly
if (field.name === undefined && field.fields) {
field.fields.forEach((subField) => {
validationPromises.push(createValidationPromise(dataToValidate[subField.name], subField));
});
} else if (field.fields) {
if (field.type === 'repeater' || field.type === 'flexible') {
const isArray = Array.isArray(dataToValidate[field.name]);
const rowCount = isArray ? dataToValidate[field.name].length : 0;
validationPromises.push(createValidationPromise(rowCount, field));
if (isArray) {
dataToValidate[field.name].forEach((rowData, i) => {
validationPromises.push(iterateFields(rowData, field.fields, `${path}${field.name}.${i}.`));
});
}
} else {
validationPromises.push(iterateFields(dataToValidate[field.name], field.fields, `${path}${field.name}.`));
}
} else {
validationPromises.push(createValidationPromise(dataToValidate[field.name], field));
}
}
});
return getErrorResults(validationPromises, path);
};
module.exports = async (data, fields) => {
try {
const errors = await iterateFields(data, fields);
if (errors.length > 0) {
throw new ValidationError(errors);
}
} catch (error) {
throw error;
}
};

View File

@@ -0,0 +1,44 @@
const { ValidationError } = require('../errors');
const { createValidationPromise, getErrorResults } = require('./utilities');
const iterateFields = async (data, fields, path = '') => {
const validationPromises = [];
Object.entries(data).forEach(([key, value]) => {
const field = fields.find(matchedField => matchedField.name === key);
if (field && value !== undefined) {
if (field.fields) {
if (field.type === 'repeater' || field.type === 'flexible') {
const isArray = Array.isArray(value);
const rowCount = isArray ? value.length : 0;
validationPromises.push(createValidationPromise(rowCount, field));
if (isArray) {
value.forEach((rowData, i) => {
validationPromises.push(iterateFields(rowData, field.fields, `${path}${field.name}.${i}.`));
});
}
} else {
validationPromises.push(iterateFields(value, field.fields, `${path}${field.name}.`));
}
} else {
validationPromises.push(createValidationPromise(value, field));
}
}
});
return getErrorResults(validationPromises, path);
};
module.exports = async (data, fields) => {
try {
const errors = await iterateFields(data, fields);
if (errors.length > 0) {
throw new ValidationError(errors);
}
} catch (error) {
throw error;
}
};

View File

@@ -1,8 +1,10 @@
const defaultMessage = 'This field is required.';
const optionsToValidatorMap = {
number: (value, options = {}) => {
const parsedValue = parseInt(value, 10);
if (typeof parsedValue !== 'number' || Number.isNaN(parsedValue)) {
if (value && (typeof parsedValue !== 'number' || Number.isNaN(parsedValue))) {
return 'Please enter a valid number.';
}
@@ -14,6 +16,10 @@ const optionsToValidatorMap = {
return `"${value}" is less than the min allowed value of ${options.min}.`;
}
if (options.required && typeof parsedValue !== 'number') {
return defaultMessage;
}
return true;
},
text: (value, options = {}) => {
@@ -25,8 +31,10 @@ const optionsToValidatorMap = {
return `This value must be longer than the minimum length of ${options.max} characters.`;
}
if (typeof value !== 'string' || (typeof value === 'string' && value.length === 0)) {
return 'This field is required.';
if (options.required) {
if (typeof value !== 'string' || (typeof value === 'string' && value.length === 0)) {
return defaultMessage;
}
}
return true;
@@ -40,13 +48,19 @@ const optionsToValidatorMap = {
return `This value must be longer than the minimum length of ${options.max} characters.`;
}
return Boolean(value);
},
email: (value) => {
if (/\S+@\S+\.\S+/.test(value)) {
return true;
if (options.required && !value) {
return defaultMessage;
}
return 'Please enter a valid email address.';
return true;
},
email: (value, options = {}) => {
if ((value && !/\S+@\S+\.\S+/.test(value))
|| (!value && options.required)) {
return 'Please enter a valid email address.';
}
return true;
},
textarea: (value, options = {}) => {
if (options.maxLength && value.length > options.maxLength) {
@@ -57,39 +71,52 @@ const optionsToValidatorMap = {
return `This value must be longer than the minimum length of ${options.max} characters.`;
}
return Boolean(value);
},
wysiwyg: (value) => {
if (value) return true;
if (options.required && !value) {
return defaultMessage;
}
return 'This field is required.';
return true;
},
code: (value) => {
if (value) return true;
wysiwyg: (value, options = {}) => {
if (options.required && !value) {
return defaultMessage;
}
return 'This field is required.';
return true;
},
checkbox: (value) => {
if (typeof value === 'boolean') {
code: (value, options = {}) => {
if (options.required && !value) {
return defaultMessage;
}
return true;
},
checkbox: (value, options = {}) => {
if ((value && typeof value !== 'boolean')
|| (options.required && typeof value !== 'boolean')) {
return 'This field can only be equal to true or false.';
}
return true;
},
date: (value, options = {}) => {
if (value && value instanceof Date) {
return true;
}
return 'This field can only be equal to true or false.';
},
date: (value) => {
if (value instanceof Date) {
return true;
if (options.required) {
return defaultMessage;
}
return `"${value}" is not a valid date.`;
},
upload: (value) => {
if (value) return true;
return 'This field is required.';
upload: (value, options = {}) => {
if (value || !options.required) return true;
return defaultMessage;
},
relationship: (value) => {
if (value) return true;
return 'This field is required.';
relationship: (value, options = {}) => {
if (value || !options.required) return true;
return defaultMessage;
},
repeater: (value, options = {}) => {
if (options.minRows && value < options.minRows) {
@@ -100,19 +127,19 @@ const optionsToValidatorMap = {
return `This field requires no more than ${options.maxRows} row(s).`;
}
if (!value) {
if (!value && options.required) {
return 'This field requires at least one row.';
}
return true;
},
select: (value) => {
if (value && value.length > 0) return true;
return 'This field is required.';
select: (value, options = {}) => {
if (value || !options.required) return true;
return defaultMessage;
},
radio: (value) => {
if (value) return true;
return 'This field is required.';
radio: (value, options = {}) => {
if (value || !options.required) return true;
return defaultMessage;
},
flexible: (value, options) => {
if (options.minRows && value < options.minRows) {
@@ -123,7 +150,7 @@ const optionsToValidatorMap = {
return `This field requires no more than ${options.maxRows} row(s).`;
}
if (!value) {
if (!value && options.required) {
return 'This field requires at least one row.';
}