-
-
Notifications
You must be signed in to change notification settings - Fork 623
/
context.ts
99 lines (85 loc) · 3.28 KB
/
context.ts
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
import * as _ from 'lodash';
import { FieldInstance, Location, Meta, ValidationError } from './base';
import { ContextItem } from './context-items';
function getDataMapKey(path: string, location: Location) {
return `${location}:${path}`;
}
export type Optional = { nullable: boolean; checkFalsy: boolean } | false;
export class Context {
private readonly _errors: ValidationError[] = [];
get errors(): ReadonlyArray<ValidationError> {
return this._errors;
}
private readonly dataMap: Map<string, FieldInstance> = new Map();
constructor(
readonly fields: string[],
readonly locations: Location[],
readonly stack: ReadonlyArray<ContextItem>,
readonly optional: Optional,
readonly message?: any,
) {}
getData(options: { requiredOnly: boolean } = { requiredOnly: false }) {
// Have to store this.optional in a const otherwise TS thinks the value could have changed
// when the functions below run
const { optional } = this;
const checks =
options.requiredOnly && optional
? [
(value: any) => value !== undefined,
(value: any) => (optional.nullable ? value != null : true),
(value: any) => (optional.checkFalsy ? value : true),
]
: [];
return _([...this.dataMap.values()])
.groupBy('originalPath')
.flatMap((instances, group) => {
const locations = _.uniqBy(instances, 'location');
// #331 - When multiple locations are involved, all of them must pass the validation.
// If none of the locations contain the field, we at least include one for error reporting.
// #458, #531 - Wildcards are an exception though: they may yield 0..* instances with different
// paths, so we may want to skip this filtering.
if (instances.length > 1 && locations.length > 1 && !group.includes('*')) {
const withValue = instances.filter(instance => instance.value !== undefined);
return withValue.length ? withValue : [instances[0]];
}
return instances;
})
.filter(instance => checks.every(check => check(instance.value)))
.valueOf();
}
addFieldInstances(instances: FieldInstance[]) {
instances.forEach(instance => {
this.dataMap.set(getDataMapKey(instance.path, instance.location), { ...instance });
});
}
setData(path: string, value: any, location: Location) {
const instance = this.dataMap.get(getDataMapKey(path, location));
if (!instance) {
throw new Error('Attempt to write data that did not pre-exist in context');
}
instance.value = value;
}
addError(message: any, value: any, meta: Meta): void;
addError(message: any, nestedErrors: ValidationError[]): void;
addError(message: any, valueOrNestedErrors: any, meta?: Meta) {
const msg = message || this.message || 'Invalid value';
if (meta) {
this._errors.push({
value: valueOrNestedErrors,
msg: typeof msg === 'function' ? msg(valueOrNestedErrors, meta) : msg,
param: meta.path,
location: meta.location,
});
} else {
this._errors.push({
msg,
param: '_error',
nestedErrors: valueOrNestedErrors,
});
}
}
}
export type ReadonlyContext = Pick<
Context,
Exclude<keyof Context, 'setData' | 'addFieldInstances' | 'addError'>
>;