Skip to content

Commit 7b24729

Browse files
authored
Merge pull request #2931 from hapijs/feat/external-helpers
feat: improve external helpers
2 parents c191b99 + e52a362 commit 7b24729

File tree

5 files changed

+1150
-610
lines changed

5 files changed

+1150
-610
lines changed

API.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,14 @@ Adds an external validation rule where:
783783
return a replacement value, `undefined` to indicate no change, or throw an error, where:
784784
- `value` - a clone of the object containing the value being validated.
785785
- `helpers` - an object with the following helpers:
786+
- `schema` - the current schema.
787+
- `linked` - if the schema is a link, the schema it links to.
788+
- `state` - the current validation state.
786789
- `prefs` - the current preferences.
790+
- `original` - the original value passed into validation before any conversions.
791+
- `error(code, [local])` - a method to generate error codes using a message code and optional local context.
792+
- `message(messages, [local])` - a method to generate an error with an internal `'external'` error code and the provided messages object to use as override. Note that this is much slower than using the preferences `messages` option but is much simpler to write when performance is not important.
793+
- `warn(code, [local])` - a method to add a warning using a message code and optional local context.
787794
- `description` - optional string used to document the purpose of the method.
788795

789796
Note that external validation rules are only called after the all other validation rules for the

lib/index.d.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -742,13 +742,20 @@ declare namespace Joi {
742742
message: (messages: LanguageMessages, local?: Context) => ErrorReport;
743743
}
744744

745-
type CustomValidator<V = any> = (value: V, helpers: CustomHelpers) => V | ErrorReport;
745+
type CustomValidator<V = any, R = V> = (value: V, helpers: CustomHelpers<R>) => R | ErrorReport;
746746

747-
interface ExternalHelpers {
747+
interface ExternalHelpers<V = any> {
748+
schema: ExtensionBoundSchema;
749+
linked: ExtensionBoundSchema | null;
750+
state: State;
748751
prefs: ValidationOptions;
752+
original: V;
753+
warn: (code: string, local?: Context) => void;
754+
error: (code: string, local?: Context) => ErrorReport;
755+
message: (messages: LanguageMessages, local?: Context) => ErrorReport;
749756
}
750757

751-
type ExternalValidationFunction<V = any> = (value: V, helpers: ExternalHelpers) => V | undefined;
758+
type ExternalValidationFunction<V = any, R = V> = (value: V, helpers: ExternalHelpers<R>) => R | undefined;
752759

753760
type SchemaLikeWithoutArray = string | number | boolean | null | Schema | SchemaMap;
754761
type SchemaLike = SchemaLikeWithoutArray | object;

lib/validator.js

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,42 +67,102 @@ exports.entryAsync = async function (value, schema, prefs) {
6767

6868
if (mainstay.externals.length) {
6969
let root = result.value;
70-
for (const { method, path, label } of mainstay.externals) {
70+
const errors = [];
71+
for (const external of mainstay.externals) {
72+
const path = external.state.path;
73+
const linked = external.schema.type === 'link' ? mainstay.links.get(external.schema) : null;
7174
let node = root;
7275
let key;
7376
let parent;
7477

78+
const ancestors = path.length ? [root] : [];
79+
const original = path.length ? Reach(value, path) : value;
80+
7581
if (path.length) {
7682
key = path[path.length - 1];
77-
parent = Reach(root, path.slice(0, -1));
83+
84+
let current = root;
85+
for (const segment of path.slice(0, -1)) {
86+
current = current[segment];
87+
ancestors.unshift(current);
88+
}
89+
90+
parent = ancestors[0];
7891
node = parent[key];
7992
}
8093

8194
try {
82-
const output = await method(node, { prefs });
95+
const createError = (code, local) => (linked || external.schema).$_createError(code, node, local, external.state, settings);
96+
const output = await external.method(node, {
97+
schema: external.schema,
98+
linked,
99+
state: external.state,
100+
prefs,
101+
original,
102+
error: createError,
103+
errorsArray: internals.errorsArray,
104+
warn: (code, local) => mainstay.warnings.push((linked || external.schema).$_createError(code, node, local, external.state, settings)),
105+
message: (messages, local) => (linked || external.schema).$_createError('external', node, local, external.state, settings, { messages })
106+
});
107+
83108
if (output === undefined ||
84109
output === node) {
85110

86111
continue;
87112
}
88113

114+
if (output instanceof Errors.Report) {
115+
mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
116+
errors.push(output);
117+
118+
if (settings.abortEarly) {
119+
break;
120+
}
121+
122+
continue;
123+
}
124+
125+
if (Array.isArray(output) &&
126+
output[Common.symbols.errors]) {
127+
mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
128+
errors.push(...output);
129+
130+
if (settings.abortEarly) {
131+
break;
132+
}
133+
134+
continue;
135+
}
136+
89137
if (parent) {
138+
mainstay.tracer.value(external.state, 'rule', node, output, 'external');
90139
parent[key] = output;
91140
}
92141
else {
142+
mainstay.tracer.value(external.state, 'rule', root, output, 'external');
93143
root = output;
94144
}
95145
}
96146
catch (err) {
97147
if (settings.errors.label) {
98-
err.message += ` (${label})`; // Change message to include path
148+
err.message += ` (${(external.label)})`; // Change message to include path
99149
}
100150

101151
throw err;
102152
}
103153
}
104154

105155
result.value = root;
156+
157+
if (errors.length) {
158+
result.error = Errors.process(errors, value, settings);
159+
160+
if (mainstay.debug) {
161+
result.error.debug = mainstay.debug;
162+
}
163+
164+
throw result.error;
165+
}
106166
}
107167

108168
if (!settings.warnings &&
@@ -514,7 +574,7 @@ internals.finalize = function (value, errors, helpers) {
514574
prefs._externals !== false) { // Disabled for matching
515575

516576
for (const { method } of schema.$_terms.externals) {
517-
state.mainstay.externals.push({ method, path: state.path, label: Errors.label(schema._flags, state, prefs) });
577+
state.mainstay.externals.push({ method, schema, state, label: Errors.label(schema._flags, state, prefs) });
518578
}
519579
}
520580

0 commit comments

Comments
 (0)