-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
model-validations.js
131 lines (120 loc) · 5.71 KB
/
model-validations.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/* eslint-disable no-console */
import validators from 'vault/utils/validators';
import { get } from '@ember/object';
/**
* used to validate properties on a class
*
* decorator expects validations object with the following shape:
* { [propertyKeyName]: [{ type, options, message, validator }] }
* each key in the validations object should refer to the property on the class to apply the validation to
* type refers to the type of validation to apply -- must be exported from validators util for lookup
* options is an optional object for given validator -- min, max, nullable etc. -- see validators in util
* message is added to the errors array and returned from the validate method if validation fails
* validator may be used in place of type to provide a function that gets executed in the validate method
* validator is useful when specific validations are needed (dependent on other class properties etc.)
* validator must be passed as function that takes the class context (this) as the only argument and returns true or false
* each property supports multiple validations provided as an array -- for example, presence and length for string
*
* validations must be invoked using the validate method which is added directly to the decorated class
* const { isValid, state } = this.model.validate();
* isValid represents the validity of the full class -- if no properties provided in the validations object are invalid this will be true
* state represents the error state of the properties defined in the validations object
* const { isValid, errors } = state[propertyKeyName];
* isValid represents the validity of the property
* errors will be populated with messages defined in the validations object when validations fail
* since a property can have multiple validations, errors is always returned as an array
*
*** basic example
*
* import Model from '@ember-data/model';
* import withModelValidations from 'vault/decorators/model-validations';
*
* const validations = { foo: [{ type: 'presence', message: 'foo is a required field' }] };
* @withModelValidations(validations)
* class SomeModel extends Model { foo = null; }
*
* const model = new SomeModel();
* const { isValid, state } = model.validate();
* -> isValid = false;
* -> state.foo.isValid = false;
* -> state.foo.errors = ['foo is a required field'];
*
*** example using custom validator
*
* const validations = { foo: [{ validator: (model) => model.bar.includes('test') ? model.foo : false, message: 'foo is required if bar includes test' }] };
* @withModelValidations(validations)
* class SomeModel extends Model { foo = false; bar = ['foo', 'baz']; }
*
* const model = new SomeModel();
* const { isValid, state } = model.validate();
* -> isValid = false;
* -> state.foo.isValid = false;
* -> state.foo.errors = ['foo is required if bar includes test'];
*/
export function withModelValidations(validations) {
return function decorator(SuperClass) {
return class ModelValidations extends SuperClass {
static _validations;
constructor() {
super(...arguments);
if (!validations || typeof validations !== 'object') {
throw new Error('Validations object must be provided to constructor for setup');
}
this._validations = validations;
}
validate() {
let isValid = true;
const state = {};
let errorCount = 0;
for (const key in this._validations) {
const rules = this._validations[key];
if (!Array.isArray(rules)) {
console.error(
`Must provide validations as an array for property "${key}" on ${this.modelName} model`
);
continue;
}
state[key] = { errors: [] };
for (const rule of rules) {
const { type, options, message, validator: customValidator } = rule;
// check for custom validator or lookup in validators util by type
const useCustomValidator = typeof customValidator === 'function';
const validator = useCustomValidator ? customValidator : validators[type];
if (!validator) {
console.error(
!type
? 'Validator not found. Define either type or pass custom validator function under "validator" key in validations object'
: `Validator type: "${type}" not found. Available validators: ${Object.keys(
validators
).join(', ')}`
);
continue;
}
const passedValidation = useCustomValidator
? validator(this)
: validator(get(this, key), options); // dot notation may be used to define key for nested property
if (!passedValidation) {
// message can also be a function
const validationMessage = typeof message === 'function' ? message(this) : message;
// consider setting a prop like validationErrors directly on the model
// for now return an errors object
state[key].errors.push(validationMessage);
if (isValid) {
isValid = false;
}
}
}
errorCount += state[key].errors.length;
state[key].isValid = !state[key].errors.length;
}
return { isValid, state, invalidFormMessage: this.generateErrorCountMessage(errorCount) };
}
generateErrorCountMessage(errorCount) {
if (errorCount < 1) return null;
// returns count specific message: 'There is an error/are N errors with this form.'
let isPlural = errorCount > 1 ? `are ${errorCount} errors` : false;
return `There ${isPlural ? isPlural : 'is an error'} with this form.`;
}
};
};
}