Closed fider-apptimia closed 4 years ago
Great question! Joiful was designed to make use of Joi's extend
functionality, which is how you add custom validation.
However, it's a bit complex, and it looks like it's not currently possible (ever since joiful v1.0.0, when we changed our API from individual decorator functions to a fluent interface). (On further analysis, it should be possible with our current API.)
Leave it with us, and we will make the necessary changes and provide an example.
Okay, here's a test I've come up with that demonstrates how to add a custom validation function. The summary is:
Joi.extend()
to define your new validation rule. This returns a new (extended) instance of Joi..custom()
method to construct a schema that makes use of your new rule. /**
* @see https://hapi.dev/family/joi/api/?v=15.1.1#extendextension
*/
it('Extending Joi for custom validation', () => {
// Custom validation functions must be added by using Joi's "extend" mechanism.
// These are utility types you may find useful to replace the return of a function.
// These types are derived from: https://stackoverflow.com/a/50014868
type ArgumentTypes<T> = T extends (...args: infer U ) => infer _R ? U: never;
type ReplaceReturnType<T, TNewReturn> = (...a: ArgumentTypes<T>) => TNewReturn;
// We are going to create a new instance of Joi, with our extended functionality: a custom validation
// function that checks if each character in a string has "alternating case" (that is, each character has a
// case different to those either side of it).
// For our own peace of mind, we're first going to update the type of the Joi instance to include our new
// schema.
interface ExtendedStringSchema extends StringSchema {
alternatingCase(): this; // We're adding this method, only for string schemas.
}
type IJoi = typeof Joi; // Need to alias this, because `interface Foo extends typeof Joi` doesn't work.
interface CustomJoi extends IJoi {
// This allows us to use our extended string schema, in place of Joi's original StringSchema.
// E.g. instead of `Joi.string()` returning `StringSchema`, it now returns `ExtendedStringSchema`.
string: ReplaceReturnType<IJoi['string'], ExtendedStringSchema>;
}
// This is our where we define our custom rule. Please read the Joi documentation for more info.
// NOTE: we must explicitly provide the type annotation of `CustomJoi`.
const customJoi: CustomJoi = Joi.extend({
base: Joi.string(), // The base Joi schema
language: { // This defines the error message returned when a rule fails validation.
alternatingCase: 'must be in alternating case', // Used as 'string.alternatingCase'
},
name: 'string', // The type (can be an existing Joi type)
rules: [
{
name: 'alternatingCase', // This is the name of the new validation function
validate(
_params: void,
value: string,
state,
options,
) { // Your validation implementation would go here.
if (value.length < 2) {
return true;
}
let lastCase = null;
for (let char of value) {
const charIsUppercase = /[A-Z]/.test(char);
if (charIsUppercase === lastCase) { // Not alternating case
// Validation failures must return a Joi error.
// You'll need to allow a suspicious use of "this" here, so that we can access the
// Joi instance's `createError()` method.
// tslint:disable-next-line:no-invalid-this
return this.createError('string.case', { v: value }, state, options);
}
lastCase = charIsUppercase;
}
return value;
},
},
],
});
// This function is how we're going to make use of our custom validator.
function alternatingCase(options: { schema: Joi.Schema, joi: typeof Joi }): Joi.Schema {
// (TODO: remove the `as CustomJoi` assertion. Requires making Joiful, JoifulOptions etc generic.)
return (options.joi as CustomJoi).string().alternatingCase();
}
const customJoiful = new Joiful({
joi: customJoi,
});
class ThingToValidate {
// Note that we must _always_ use our own `customJoiful` for all decorators, instead of importing them
// directly from Joiful (e.g. `customJoiful.string()` vs `jf.string()`)
// Failing to do so means Joiful will use the default instance of Joi, which could cause inconsistent
// behaviour, and prevent us from using our custom validator.
@customJoiful.string().custom(alternatingCase)
public propertyToValidate: string;
constructor(
propertyToValidate: string,
) {
this.propertyToValidate = propertyToValidate;
}
}
// Execute & verify
let instance = new ThingToValidate(
'aBcDeFgH',
);
expect(instance).toBeValid();
instance = new ThingToValidate(
'AbCdEfGh',
);
expect(instance).toBeValid();
instance = new ThingToValidate(
'abcdefgh',
);
expect(instance).not.toBeValid();
});
// -------------
// jf.ts
// -------------
import Joiful from "joiful";
export = new Joiful();
// -------------
// app.ts
// -------------
// import * as jf from "joiful"; <-- we're replacing this
import * as jf from "./jf"; // <-- this is our custom wrapper module
class Foo {
jf.string()
bar: string;
}
I hope this helps, let me know if you have any more questions.
I frogot to mention, you also need to pass your custom Joi instance to your validator instance.
const validator = new Validator({
joi: customJoi,
});
There was a bug preventing this from working, which was fixed in v2.0.1.
I'll close this issue for now, please reach out if you have further questions.
Is it possible to provide function as custom validator like below?:
If no, then how to achieve it? (I know only how to create custom validator basing on predefined
joiful
decorators)