-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
142 lines (122 loc) · 4.29 KB
/
index.js
File metadata and controls
142 lines (122 loc) · 4.29 KB
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
132
133
134
135
136
137
138
139
140
141
142
const EMPTY_VALUE = [null, undefined, ''];
export const EMPTY_MANDATORY_FIELD_ERROR = 'empty_mandatory_field';
export const STOP_OPTION = Object.freeze({
FIELDS: 'fields',
TESTS: 'tests'
})
import get from 'lodash.get';
import set from 'lodash.set';
const fieldIsEmpty = data =>
typeof data !== 'number' && (data === false || !data?.length); //this covers required true, null, undefined, '' and empty array
function validateFields(fields) {
for (const field of fields) {
if (field.stopOnFailure && !['tests', 'fields'].includes(field.stopOnFailure)) {
throw new Error(`field.stopOnFailure's value, if specified must be either 'tests' or 'fields'. Please use the exported STOP_OPTION.TESTS or STOP_OPTION.FIELDS for consistency`)
}
if (field.stopOnSuccess && !['tests', 'fields'].includes(field.stopOnSuccess)) {
throw new Error(`field.stopOnSuccess's value, if specified must be either 'tests' or 'fields'. Please use the exported STOP_OPTION.TESTS or STOP_OPTION.FIELDS for consistency`)
}
if (!field.name) {
throw new Error('field.name must be specified');
}
}
return fields;
}
async function evaluateFields({ fields, context, out, data, base = '' }) {
let valid = true;
const invalidate = (name, message) => {
valid = false;
set(out, name, message);
}
const setValid = (name) => {
set(out, name, true);
}
const mandatoryFieldFault = (name, message) => {
invalidate(name, message || this.mandatoryFieldError);
}
for (let field of fields) {
const fieldData = get(data, field.name);
const isEmpty = await (field.emptyTest || fieldIsEmpty)(fieldData)
const aContext = Object.freeze(Object.assign({
name: field.name,
data
}, context));
if (field.skipIf && field.skipIf(aContext)) {
continue;
}
if (field.isOptional && isEmpty) {
continue; //empty/false but not mandatory. no issue
}
const path = base.split('.').concat(field.name.split('.'), '').filter(seg => !!seg).join('.');
if (!field.isOptional && isEmpty) {
mandatoryFieldFault(path, field.emptyFieldMessage);
continue;
}
//first set the name valid. This will be overridden on test failure
setValid(path);
//check for subfields
if (Array.isArray(get(data, field.name)) && field.fields) {
let idx = 0;
for (const subData of get(data, field.name)) {
valid = await evaluateFields.call(this, {
fields: field.fields,
data: subData,
out,
context,
base: `${path}.${idx}`
}) && valid;
idx++;
}
}
if (!field.tests) {
continue;
}
let stopFields = false;
for (let test of field.tests) {
if (!(await test.fn(fieldData, aContext))) {
invalidate(path, test.message);
if (field.stopOnFailure) {
if (field.stopOnFailure === 'fields') {
stopFields = true;
}
break;
}
continue;
}
if (field.stopOnSuccess) {
if (field.stopOnSuccess === 'fields') {
stopFields = true;
}
break;
}
}
if (stopFields) {
break;
}
}
return valid;
}
export default class Validation {
#model = null
#fields = null
mandatoryFieldError = EMPTY_MANDATORY_FIELD_ERROR;
constructor(model, fields) {
this.#model = model;
this.#fields = validateFields(fields);
}
withMandatoryFieldError(text) {
this.mandatoryFieldError = text;
return this;
}
async validate(data, context = {}) {
const out = Object.assign({}, this.#model.fields);
const valid = await evaluateFields.call(this, {
data,
fields: this.#fields,
out,
context
});
Object.assign(this.#model.fields, out);
return (this.#model.isValid = valid);
}
}