Work on form errors & validation

This commit is contained in:
SachaG 2018-03-25 10:54:45 +09:00
parent 8d686ae9e1
commit 1d7cef5556
11 changed files with 383 additions and 135 deletions

View file

@ -84,6 +84,7 @@ Datatable.propTypes = {
showSearch: PropTypes.bool, showSearch: PropTypes.bool,
newFormOptions: PropTypes.object, newFormOptions: PropTypes.object,
editFormOptions: PropTypes.object, editFormOptions: PropTypes.object,
emptyState: PropTypes.object,
} }
Datatable.defaultProps = { Datatable.defaultProps = {

View file

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { registerComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
const FieldErrors = ({ errors }) => (
<ul className="form-input-errors">
{errors.map((error, index) => (
<li key={index}>
{error.message || (
<FormattedMessage
id={`errors.${error.data.type}`}
values={{ ...error.data }}
defaultMessage={JSON.stringify(error)}
/>
)}
</li>
))}
</ul>
);
registerComponent('FieldErrors', FieldErrors);

View file

@ -164,24 +164,48 @@ class Form extends Component {
Get a field's value in a document // TODO: maybe not needed? Get a field's value in a document // TODO: maybe not needed?
*/ */
getValue = (fieldName, document) => { getValue = (fieldName, fieldSchema, document) => {
if (typeof document[fieldName] !== 'undefined' && document[fieldName] !== null) { if (typeof document[fieldName] !== 'undefined' && document[fieldName] !== null) {
return document[fieldName];
let value = document[fieldName];
// convert value type if needed
if (fieldSchema.type.definitions[0].type === Number) value = Number(value);
// if value is an array of objects ({_id: '123'}, {_id: 'abc'}), flatten it into an array of strings (['123', 'abc'])
// fallback to item itself if item._id is not defined (ex: item is not an object or item is just {slug: 'xxx'})
// if (Array.isArray(field.value)) {
// field.value = field.value.map(item => item._id || item);
// }
// TODO: not needed anymore?
return value;
} }
return null; return null;
}; };
/* /*
Given an array field, get its nested schema
*/
getNestedSchema = (fieldName) => {
const arrayItemSchema = this.getSchema()[`${fieldName}.$`];
return arrayItemSchema && arrayItemSchema.type.definitions[0].type._schema;
}
/*
Given a field's name, its schema, document, and parent, create the Given a field's name, its schema, document, and parent, create the
complete field object to be passed to the component complete field object to be passed to the component
*/ */
createField = (fieldName, fieldSchema, document, parentFieldName) => { createField = (fieldName, fieldSchema, document, parentFieldName, parentPath) => {
// console.log('// createField', fieldName)
const fieldPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
// console.log('// createField', fieldPath)
// console.log(fieldSchema) // console.log(fieldSchema)
// console.log(document) // console.log(document)
// console.log('-> nested: ', fieldSchema.type.singleType === Array) // console.log('-> nested: ', !!this.getNestedSchema(fieldName))
// store fieldSchema object in this.fieldSchemas // store fieldSchema object in this.fieldSchemas
this.fieldSchemas[fieldName] = fieldSchema; this.fieldSchemas[fieldName] = fieldSchema;
@ -191,6 +215,7 @@ class Form extends Component {
// intialize properties // intialize properties
let field = { let field = {
name: fieldName, name: fieldName,
path: fieldPath,
datatype: fieldSchema.type, datatype: fieldSchema.type,
control: fieldSchema.control, control: fieldSchema.control,
layout: this.props.layout, layout: this.props.layout,
@ -205,21 +230,10 @@ class Form extends Component {
field.label = this.getLabel(fieldName); field.label = this.getLabel(fieldName);
// note: for nested fields, value will be null here and set by FormNested later // note: for nested fields, value will be null here and set by FormNested later
const fieldValue = this.getValue(fieldName, document); const fieldValue = this.getValue(fieldName, fieldSchema, document);
// add value // add value
if (fieldValue) { if (fieldValue) {
field.value = fieldValue; field.value = fieldValue;
// convert value type if needed
if (fieldSchema.type.definitions[0].type === Number) field.value = Number(field.value);
// if value is an array of objects ({_id: '123'}, {_id: 'abc'}), flatten it into an array of strings (['123', 'abc'])
// fallback to item itself if item._id is not defined (ex: item is not an object or item is just {slug: 'xxx'})
// if (Array.isArray(field.value)) {
// field.value = field.value.map(item => item._id || item);
// }
// TODO: not needed anymore?
} }
// backward compatibility from 'autoform' to 'form' // backward compatibility from 'autoform' to 'form'
@ -301,25 +315,23 @@ class Form extends Component {
// add document // add document
field.document = this.getDocument(); field.document = this.getDocument();
// add error state // add any relevant errors
const validationError = _.findWhere(this.state.errors, { name: 'app.validation_error' }); // const fieldErrors = _.filter(this.state.errors, error => error.data.name === fieldName);
if (validationError) { // if (fieldErrors) {
const fieldErrors = _.filter(validationError.data.errors, error => error.data.fieldName === fieldName); // field.errors = fieldErrors.map(error => ({ ...error, message: this.getErrorMessage(error) }));
if (fieldErrors) { // }
field.errors = fieldErrors.map(error => ({ ...error, message: this.getErrorMessage(error) }));
}
}
// nested fields: set control to "nested" // nested fields: set control to "nested"
if (fieldSchema.type.singleType === Array) { const nestedSchema = this.getNestedSchema(fieldName);
if (nestedSchema) {
field.nestedSchema = nestedSchema;
field.control = 'nested'; field.control = 'nested';
// get nested schema // get nested schema
field.nestedSchema = this.getSchema()[`${fieldName}.$`].type.definitions[0].type._schema; // TODO: do this better
// for each nested field, get field object by calling createField recursively // for each nested field, get field object by calling createField recursively
field.nestedFields = this.getFieldNames(field.nestedSchema).map(subFieldName => { field.nestedFields = this.getFieldNames(field.nestedSchema).map(subFieldName => {
const subFieldSchema = field.nestedSchema[subFieldName]; const subFieldSchema = field.nestedSchema[subFieldName];
return this.createField(subFieldName, subFieldSchema, document, fieldName); return this.createField(subFieldName, subFieldSchema, document, fieldName, fieldPath);
}); });
} }
@ -435,41 +447,7 @@ class Form extends Component {
} }
}; };
/*
Render errors
*/
renderErrors = () => {
return (
<div className="form-errors">
{this.state.errors.map((error, index) => {
let message;
if (error.data && error.data.errors) {
// this error is a "multi-error" with multiple sub-errors
message = error.data.errors.map(error => {
return {
content: this.getErrorMessage(error),
data: error.data,
};
});
} else {
// this is a regular error
message = {
content:
error.message ||
this.context.intl.formatMessage({ id: error.id, defaultMessage: error.id }, error.data),
};
}
return <Components.FormFlash key={index} message={message} type="error" />;
})}
</div>
);
};
// --------------------------------------------------------------------- // // --------------------------------------------------------------------- //
// ------------------------------- Context ----------------------------- // // ------------------------------- Context ----------------------------- //
@ -486,7 +464,7 @@ class Form extends Component {
// add error to state // add error to state
this.setState(prevState => ({ this.setState(prevState => ({
errors: [...prevState.errors, graphQLError], errors: [...prevState.errors, ...graphQLError.data.errors],
})); }));
}; };
@ -560,6 +538,7 @@ class Form extends Component {
addToSubmitForm: this.addToSubmitForm, addToSubmitForm: this.addToSubmitForm,
addToSuccessForm: this.addToSuccessForm, addToSuccessForm: this.addToSuccessForm,
addToFailureForm: this.addToFailureForm, addToFailureForm: this.addToFailureForm,
errors: this.state.errors,
}; };
}; };
@ -584,7 +563,6 @@ class Form extends Component {
this.addToDeletedValues(path); this.addToDeletedValues(path);
} else { } else {
set(prevState.currentValues, path, value); set(prevState.currentValues, path, value);
// dot.str(path, value, prevState.currentValues);
} }
}); });
return prevState; return prevState;
@ -783,7 +761,7 @@ class Form extends Component {
return ( return (
<div className={'document-' + this.getFormType()}> <div className={'document-' + this.getFormType()}>
<Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form"> <Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form">
{this.renderErrors()} <Components.FormErrors errors={this.state.errors}/>
{fieldGroups.map(group => ( {fieldGroups.map(group => (
<Components.FormGroup key={group.name} {...group} updateCurrentValues={this.updateCurrentValues} /> <Components.FormGroup key={group.name} {...group} updateCurrentValues={this.updateCurrentValues} />
@ -870,6 +848,7 @@ Form.childContextTypes = {
clearForm: PropTypes.func, clearForm: PropTypes.func,
getDocument: PropTypes.func, getDocument: PropTypes.func,
submitForm: PropTypes.func, submitForm: PropTypes.func,
errors: PropTypes.array,
}; };
module.exports = Form; module.exports = Form;

View file

@ -52,7 +52,7 @@ class FormComponent extends PureComponent {
renderComponent() { renderComponent() {
// see https://facebook.github.io/react/warnings/unknown-prop.html // see https://facebook.github.io/react/warnings/unknown-prop.html
const { control, group, updateCurrentValues, document, beforeComponent, afterComponent, limit, errors, nestedSchema, nestedFields, datatype, parentFieldName, itemIndex, ...rest } = this.props; // eslint-disable-line const { control, group, updateCurrentValues, document, beforeComponent, afterComponent, limit, errors, nestedSchema, nestedFields, datatype, parentFieldName, itemIndex, path, ...rest } = this.props; // eslint-disable-line
const properties = { const properties = {
value: '', // default value, will be overridden by `rest` if real value has been passed down through props value: '', // default value, will be overridden by `rest` if real value has been passed down through props
@ -72,7 +72,7 @@ class FormComponent extends PureComponent {
switch (this.props.control) { switch (this.props.control) {
case 'nested': case 'nested':
return <Components.FormNested updateCurrentValues={updateCurrentValues} nestedSchema={nestedSchema} nestedFields={nestedFields} {...properties}/>; return <Components.FormNested path={path} updateCurrentValues={updateCurrentValues} nestedSchema={nestedSchema} nestedFields={nestedFields} datatype={datatype} {...properties}/>;
case 'number': case 'number':
return <Components.FormComponentNumber {...properties}/>; return <Components.FormComponentNumber {...properties}/>;
@ -145,12 +145,9 @@ class FormComponent extends PureComponent {
} }
} }
renderErrors = () => { getErrors = () => {
return ( const fieldErrors = this.context.errors.filter(error => error.data.name === this.props.path);
<ul className='form-input-errors'> return fieldErrors;
{this.props.errors.map((error, index) => <li key={index}>{error.message}</li>)}
</ul>
)
} }
showClear = () => { showClear = () => {
@ -175,14 +172,14 @@ class FormComponent extends PureComponent {
render() { render() {
const hasErrors = this.props.errors && this.props.errors.length; const hasErrors = this.getErrors() && this.getErrors().length;
const inputClass = classNames('form-input', `input-${this.props.name}`, `form-component-${this.props.control || 'default'}`,{'input-error': hasErrors}); const inputClass = classNames('form-input', `input-${this.props.name}`, `form-component-${this.props.control || 'default'}`,{'input-error': hasErrors});
return ( return (
<div className={inputClass}> <div className={inputClass}>
{this.props.beforeComponent ? this.props.beforeComponent : null} {this.props.beforeComponent ? this.props.beforeComponent : null}
{this.renderComponent()} {this.renderComponent()}
{hasErrors ? this.renderErrors() : null} {hasErrors ? <Components.FieldErrors errors={this.getErrors()}/> : null}
{this.showClear() ? this.renderClear() : null} {this.showClear() ? this.renderClear() : null}
{this.props.limit ? <div className={classNames('form-control-limit', {danger: this.state.limit < 10})}>{this.state.limit}</div> : null} {this.props.limit ? <div className={classNames('form-control-limit', {danger: this.state.limit < 10})}>{this.state.limit}</div> : null}
{this.props.afterComponent ? this.props.afterComponent : null} {this.props.afterComponent ? this.props.afterComponent : null}
@ -209,6 +206,7 @@ FormComponent.propTypes = {
FormComponent.contextTypes = { FormComponent.contextTypes = {
intl: intlShape, intl: intlShape,
addToDeletedValues: PropTypes.func, addToDeletedValues: PropTypes.func,
errors: PropTypes.array,
}; };
registerComponent('FormComponent', FormComponent); registerComponent('FormComponent', FormComponent);

View file

@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { registerComponent } from 'meteor/vulcan:core';
import { FormattedMessage } from 'meteor/vulcan:i18n';
import Alert from 'react-bootstrap/lib/Alert';
const FormErrors = ({ errors }) => (
<div className="form-errors">
{!!errors.length && (
<Alert className="flash-message" bsStyle="danger">
<ul>
{errors.map((error, index) => (
<li key={index}>
{error.message || (
<FormattedMessage
id={`errors.${error.data.type}`}
values={{ ...error.data }}
defaultMessage={JSON.stringify(error)}
/>
)}
</li>
))}
</ul>
</Alert>
)}
</div>
);
registerComponent('FormErrors', FormErrors);
// /*
// Render errors
// */
// renderErrors = () => {
// return (
// <div className="form-errors">
// {this.state.errors.map((error, index) => {
// let message;
// if (error.data && error.data.errors) {
// // this error is a "multi-error" with multiple sub-errors
// message = error.data.errors.map(error => {
// return {
// content: this.getErrorMessage(error),
// data: error.data,
// };
// });
// } else {
// // this is a regular error
// message = {
// content:
// error.message ||
// this.context.intl.formatMessage({ id: error.id, defaultMessage: error.id }, error.data),
// };
// }
// return <Components.FormFlash key={index} message={message} type="error" />;
// })}
// </div>
// );
// };

View file

@ -3,14 +3,27 @@ import PropTypes from 'prop-types';
import { Components, registerComponent } from 'meteor/vulcan:core'; import { Components, registerComponent } from 'meteor/vulcan:core';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
const FormNestedItem = ({ isDeleted, nestedFields, name, subDocument, removeItem, ...props }) => { const FormNestedItem = (
{ isDeleted, nestedFields, name, path, subDocument, removeItem, itemIndex, ...props },
{ errors }
) => {
return ( return (
<div className={`form-nested-item ${isDeleted ? 'form-nested-item-deleted' : ''}`}> <div className={`form-nested-item ${isDeleted ? 'form-nested-item-deleted' : ''}`}>
<div className="form-nested-item-inner"> <div className="form-nested-item-inner">
{nestedFields.map((field, i) => { {nestedFields.map((field, i) => {
// note: default value to '' to avoid uncontrolled component error // note: default value to '' to avoid uncontrolled component error
const value = subDocument && subDocument[field.name] || ''; let value = (subDocument && subDocument[field.name]) || '';
return <Components.FormComponent key={i} {...props} {...field} value={value} />; if (props.control === 'number') value = Number(value);
return (
<Components.FormComponent
key={i}
{...props}
{...field}
path={`${path}.${field.name}`}
value={value}
itemIndex={itemIndex}
/>
);
})} })}
</div> </div>
<div className="form-nested-item-remove"> <div className="form-nested-item-remove">
@ -23,21 +36,24 @@ const FormNestedItem = ({ isDeleted, nestedFields, name, subDocument, removeItem
</Button> </Button>
</div> </div>
<div className="form-nested-item-deleted-overlay"/> <div className="form-nested-item-deleted-overlay" />
</div> </div>
); );
}; };
FormNestedItem.contextTypes = {
errors: PropTypes.array,
};
registerComponent('FormNestedItem', FormNestedItem); registerComponent('FormNestedItem', FormNestedItem);
class FormNested extends PureComponent { class FormNested extends PureComponent {
addItem = () => { addItem = () => {
this.props.updateCurrentValues({[`${this.props.name}.${this.props.value.length}`] : {}}); this.props.updateCurrentValues({ [`${this.props.path}.${this.props.value.length}`]: {} });
}; };
removeItem = index => { removeItem = index => {
this.props.updateCurrentValues({[`${this.props.name}.${index}`] : null}); this.props.updateCurrentValues({ [`${this.props.path}.${index}`]: null });
}; };
/* /*
@ -47,7 +63,7 @@ class FormNested extends PureComponent {
look for the presence of 'addresses.1') look for the presence of 'addresses.1')
*/ */
isDeleted = index => { isDeleted = index => {
return this.context.deletedValues.includes(`${this.props.name}.${index}`); return this.context.deletedValues.includes(`${this.props.path}.${index}`);
}; };
render() { render() {
@ -56,13 +72,22 @@ class FormNested extends PureComponent {
<label className="control-label col-sm-3">{this.props.label}</label> <label className="control-label col-sm-3">{this.props.label}</label>
<div className="col-sm-9"> <div className="col-sm-9">
{this.props.value && {this.props.value &&
this.props.value.map((subDocument, i) => ( this.props.value.map(
!this.isDeleted(i) && <FormNestedItem key={i} itemIndex={i} {...this.props} subDocument={subDocument} removeItem={() => {this.removeItem(i)}} /> (subDocument, i) =>
))} !this.isDeleted(i) && (
<Button <FormNestedItem
bsStyle="success" {...this.props}
onClick={this.addItem} key={i}
> itemIndex={i}
subDocument={subDocument}
path={`${this.props.path}.${i}`}
removeItem={() => {
this.removeItem(i);
}}
/>
)
)}
<Button bsStyle="success" onClick={this.addItem}>
</Button> </Button>
</div> </div>
@ -72,7 +97,7 @@ class FormNested extends PureComponent {
} }
FormNested.contextTypes = { FormNested.contextTypes = {
deletedValues: PropTypes.array deletedValues: PropTypes.array,
} };
registerComponent('FormNested', FormNested); registerComponent('FormNested', FormNested);

View file

@ -12,6 +12,8 @@ import '../components/bootstrap/Url.jsx';
import '../components/bootstrap/Date.jsx'; import '../components/bootstrap/Date.jsx';
import '../components/Flash.jsx'; import '../components/Flash.jsx';
import '../components/FieldErrors.jsx';
import '../components/FormErrors.jsx';
import '../components/FormComponent.jsx'; import '../components/FormComponent.jsx';
import '../components/FormNested.jsx'; import '../components/FormNested.jsx';
import '../components/FormGroup.jsx'; import '../components/FormGroup.jsx';

View file

@ -134,4 +134,7 @@ addStrings('en', {
"admin": "Admin", "admin": "Admin",
"notifications": "Notifications", "notifications": "Notifications",
"errors.expectedType": `Expected a field of type {dataType}, got “{value}” instead.`,
"errors.required": `Field “{name}” is required.`,
}); });

View file

@ -6,10 +6,10 @@ String.prototype.replaceAll = function(search, replacement) {
return target.replace(new RegExp(search, 'g'), replacement); return target.replace(new RegExp(search, 'g'), replacement);
}; };
const FormattedMessage = ({ id, values, defaultMessage, html = false }) => { const FormattedMessage = ({ id, values, defaultMessage = '', html = false }) => {
const messages = Strings[getSetting('locale', 'en')] || {}; const messages = Strings[getSetting('locale', 'en')] || {};
let message = messages[id] || defaultMessage; let message = messages[id] || defaultMessage;
if (values) { if (message && values) {
_.forEach(values, (value, key) => { _.forEach(values, (value, key) => {
message = message.replaceAll(`{${key}}`, value); message = message.replaceAll(`{${key}}`, value);
}); });

View file

@ -10,7 +10,6 @@
*/ */
export const validateDocument = (document, collection, context) => { export const validateDocument = (document, collection, context) => {
const { Users, currentUser } = context; const { Users, currentUser } = context;
const schema = collection.simpleSchema()._schema; const schema = collection.simpleSchema()._schema;
@ -18,14 +17,13 @@ export const validateDocument = (document, collection, context) => {
// Check validity of inserted document // Check validity of inserted document
_.forEach(document, (value, fieldName) => { _.forEach(document, (value, fieldName) => {
const fieldSchema = schema[fieldName]; const fieldSchema = schema[fieldName];
// 1. check that the current user has permission to insert each field // 1. check that the current user has permission to insert each field
if (!fieldSchema || !Users.canInsertField (currentUser, fieldSchema)) { if (!fieldSchema || !Users.canInsertField(currentUser, fieldSchema)) {
validationErrors.push({ validationErrors.push({
id: 'app.disallowed_property_detected', id: 'app.disallowed_property_detected',
fieldName fieldName,
}); });
} }
@ -33,27 +31,24 @@ export const validateDocument = (document, collection, context) => {
if (fieldSchema.limit && value.length > fieldSchema.limit) { if (fieldSchema.limit && value.length > fieldSchema.limit) {
validationErrors.push({ validationErrors.push({
id: 'app.field_is_too_long', id: 'app.field_is_too_long',
data: {fieldName, limit: fieldSchema.limit} data: { fieldName, limit: fieldSchema.limit },
}); });
} }
// 3. check that fields have the proper type // 3. check that fields have the proper type
// TODO // TODO
}); });
// 4. check that required fields have a value // 4. check that required fields have a value
_.keys(schema).forEach(fieldName => { _.keys(schema).forEach(fieldName => {
const fieldSchema = schema[fieldName]; const fieldSchema = schema[fieldName];
if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') { if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') {
validationErrors.push({ validationErrors.push({
id: 'app.required_field_missing', id: 'app.required_field_missing',
data: {fieldName} data: { fieldName },
}); });
} }
}); });
// 5. still run SS validation for now for backwards compatibility // 5. still run SS validation for now for backwards compatibility
@ -64,13 +59,12 @@ export const validateDocument = (document, collection, context) => {
console.log(error); console.log(error);
validationErrors.push({ validationErrors.push({
id: 'app.schema_validation_error', id: 'app.schema_validation_error',
data: {message: error.message} data: { message: error.message },
}); });
} }
return validationErrors; return validationErrors;
} };
/* /*
@ -84,7 +78,6 @@ export const validateDocument = (document, collection, context) => {
*/ */
export const validateModifier = (modifier, document, collection, context) => { export const validateModifier = (modifier, document, collection, context) => {
const { Users, currentUser } = context; const { Users, currentUser } = context;
const schema = collection.simpleSchema()._schema; const schema = collection.simpleSchema()._schema;
const set = modifier.$set; const set = modifier.$set;
@ -94,61 +87,223 @@ export const validateModifier = (modifier, document, collection, context) => {
// 1. check that the current user has permission to edit each field // 1. check that the current user has permission to edit each field
const modifiedProperties = _.keys(set).concat(_.keys(unset)); const modifiedProperties = _.keys(set).concat(_.keys(unset));
modifiedProperties.forEach(function (fieldName) { modifiedProperties.forEach(function(fieldName) {
var field = schema[fieldName]; var field = schema[fieldName];
if (!field || !Users.canEditField(currentUser, field, document)) { if (!field || !Users.canEditField(currentUser, field, document)) {
validationErrors.push({ validationErrors.push({
id: 'app.disallowed_property_detected', id: 'app.disallowed_property_detected',
fieldName data: {name: fieldName},
}); });
} }
}); });
// Check validity of set modifier // Check validity of set modifier
_.forEach(set, (value, fieldName) => { _.forEach(set, (value, fieldName) => {
const fieldSchema = schema[fieldName]; const fieldSchema = schema[fieldName];
// 2. check field lengths // 2. check field lengths
if (fieldSchema.limit && value.length > fieldSchema.limit) { if (fieldSchema.limit && value.length > fieldSchema.limit) {
validationErrors.push({ validationErrors.push({
id: 'app.field_is_too_long', id: 'app.field_is_too_long',
data: {fieldName, limit: fieldSchema.limit} data: { name: fieldName, limit: fieldSchema.limit },
}); });
} }
// 3. check that fields have the proper type // 3. check that fields have the proper type
// TODO // TODO
});
// // 4. check that required fields have a value
// // when editing, we only want to require fields that are actually part of the form
// // so we make sure required keys are present in the $unset object
// _.keys(schema).forEach(fieldName => {
// const fieldSchema = schema[fieldName];
// if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') {
// validationErrors.push({
// id: 'app.required_field_missing',
// data: { name: fieldName },
// });
// }
// });
// 5. still run SS validation for now for backwards compatibility
const validationContext = collection.simpleSchema().newContext();
validationContext.validate({ $set: set, $unset: unset }, { modifier: true });
if (!validationContext.isValid()) {
const errors = validationContext.validationErrors();
console.log('// validationContext');
console.log(validationContext.isValid());
console.log(errors);
errors.forEach(error => {
// eslint-disable-next-line no-console
// console.log(error);
validationErrors.push({
id: 'app.schema_validation_error',
data: error,
});
});
}
return validationErrors;
};
/*
The following versions were written to be more SimpleSchema-agnostic, but
are not currently used
*/
/*
If document is not trusted, run validation steps:
1. Check that the current user has permission to edit each field
2. Check field lengths
3. Check field types
4. Check for missing fields
5. Run SimpleSchema validation step (for now)
*/
export const validateDocumentNotUsed = (document, collection, context) => {
const { Users, currentUser } = context;
const schema = collection.simpleSchema()._schema;
let validationErrors = [];
// Check validity of inserted document
_.forEach(document, (value, fieldName) => {
const fieldSchema = schema[fieldName];
// 1. check that the current user has permission to insert each field
if (!fieldSchema || !Users.canInsertField(currentUser, fieldSchema)) {
validationErrors.push({
id: 'app.disallowed_property_detected',
fieldName,
});
}
// 2. check field lengths
if (fieldSchema.limit && value.length > fieldSchema.limit) {
validationErrors.push({
id: 'app.field_is_too_long',
data: { fieldName, limit: fieldSchema.limit },
});
}
// 3. check that fields have the proper type
// TODO
});
// 4. check that required fields have a value
_.keys(schema).forEach(fieldName => {
const fieldSchema = schema[fieldName];
if ((fieldSchema.required || !fieldSchema.optional) && typeof document[fieldName] === 'undefined') {
validationErrors.push({
id: 'app.required_field_missing',
data: { fieldName },
});
}
});
// 5. still run SS validation for now for backwards compatibility
try {
collection.simpleSchema().validate(document);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
validationErrors.push({
id: 'app.schema_validation_error',
data: { message: error.message },
});
}
return validationErrors;
};
/*
If document is not trusted, run validation steps:
1. Check that the current user has permission to insert each field
2. Check field lengths
3. Check field types
4. Check for missing fields
5. Run SimpleSchema validation step (for now)
*/
export const validateModifierNotUsed = (modifier, document, collection, context) => {
const { Users, currentUser } = context;
const schema = collection.simpleSchema()._schema;
const set = modifier.$set;
const unset = modifier.$unset;
let validationErrors = [];
// 1. check that the current user has permission to edit each field
const modifiedProperties = _.keys(set).concat(_.keys(unset));
modifiedProperties.forEach(function(fieldName) {
var field = schema[fieldName];
if (!field || !Users.canEditField(currentUser, field, document)) {
validationErrors.push({
id: 'app.disallowed_property_detected',
data: {name: fieldName},
});
}
});
// Check validity of set modifier
_.forEach(set, (value, fieldName) => {
const fieldSchema = schema[fieldName];
// 2. check field lengths
if (fieldSchema.limit && value.length > fieldSchema.limit) {
validationErrors.push({
id: 'app.field_is_too_long',
data: { name: fieldName, limit: fieldSchema.limit },
});
}
// 3. check that fields have the proper type
// TODO
}); });
// 4. check that required fields have a value // 4. check that required fields have a value
// when editing, we only want to require fields that are actually part of the form // when editing, we only want to require fields that are actually part of the form
// so we make sure required keys are present in the $unset object // so we make sure required keys are present in the $unset object
_.keys(schema).forEach(fieldName => { _.keys(schema).forEach(fieldName => {
const fieldSchema = schema[fieldName]; const fieldSchema = schema[fieldName];
if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') { if (unset[fieldName] && (fieldSchema.required || !fieldSchema.optional) && typeof set[fieldName] === 'undefined') {
validationErrors.push({ validationErrors.push({
id: 'app.required_field_missing', id: 'app.required_field_missing',
data: {fieldName} data: { name: fieldName },
}); });
} }
}); });
// 5. still run SS validation for now for backwards compatibility // 5. still run SS validation for now for backwards compatibility
try { const validationContext = collection.simpleSchema().newContext();
collection.simpleSchema().validate({$set: set, $unset: unset}, { modifier: true }); validationContext.validate({ $set: set, $unset: unset }, { modifier: true });
} catch (error) {
// eslint-disable-next-line no-console if (!validationContext.isValid()) {
console.log(error); const errors = validationContext.validationErrors();
validationErrors.push({ console.log('// validationContext');
id: 'app.schema_validation_error', console.log(validationContext.isValid());
data: {message: error.message} console.log(errors);
errors.forEach(error => {
// eslint-disable-next-line no-console
// console.log(error);
validationErrors.push({
id: 'app.schema_validation_error',
data: error,
});
}); });
} }
return validationErrors; return validationErrors;
} };

View file

@ -130,9 +130,9 @@ export const editMutation = async ({ collection, documentId, set = {}, unset = {
debug('// editMutation'); debug('// editMutation');
debug('// collectionName: ', collection._name); debug('// collectionName: ', collection._name);
debug('// documentId: ', documentId); debug('// documentId: ', documentId);
// debug('// set: ', set); debug('// set: ', set);
// debug('// unset: ', unset); debug('// unset: ', unset);
// debug('// document: ', document); debug('// document: ', document);
if (validate) { if (validate) {