Skip to content

Commit bfd463f

Browse files
committed
feat: introduced JOI validator
1 parent 2d9f991 commit bfd463f

File tree

9 files changed

+349
-2
lines changed

9 files changed

+349
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 6.10.0 (master)
2+
3+
- Introduced `JOI` validation plugin and driver.
4+
- Added `ValidatorConstructor` inferface
5+
16
# 6.9.4 (master)
27

38
- Fix: #636

package-lock.json

Lines changed: 99 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"license": "MIT",
44
"version": "0.0.0-development",
55
"author": "Claudio Savino <claudio.savino@me.com> (https://twitter.com/foxhound87)",
6-
"description": "Automagically manage React forms state and automatic validation with MobX.",
6+
"description": "Reactive MobX Form State Management",
77
"homepage": "https://github.com/foxhound87/mobx-react-form#readme",
88
"main": "./lib/index.js",
99
"types": "./lib/index.d.ts",
@@ -95,6 +95,7 @@
9595
"eslint": "^8.35.0",
9696
"eslint-plugin-import": "^2.27.5",
9797
"husky": "0.13.1",
98+
"joi": "^17.13.3",
9899
"json-loader": "0.5.4",
99100
"lodash-webpack-plugin": "^0.11.6",
100101
"mobx": "^6.3.3",

src/Validator.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ValidatorInterface, {
77
ValidationPlugin,
88
ValidationPluginInterface,
99
ValidationPlugins,
10+
ValidatorConstructor,
1011
} from "./models/ValidatorInterface";
1112
import { FormInterface } from "./models/FormInterface";
1213
import { FieldInterface } from "./models/FieldInterface";
@@ -25,11 +26,12 @@ export default class Validator implements ValidatorInterface {
2526
svk: undefined,
2627
yup: undefined,
2728
zod: undefined,
29+
joi: undefined,
2830
};
2931

3032
error: string | null = null;
3133

32-
constructor(obj: any = {}) {
34+
constructor(obj: ValidatorConstructor) {
3335
makeObservable(this, {
3436
error: observable,
3537
validate: action,

src/models/ValidatorInterface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import {FieldInterface} from "./FieldInterface";
44
import {FormInterface} from "./FormInterface";
55
import {StateInterface} from "./StateInterface";
66

7+
export interface ValidatorConstructor {
8+
form: FormInterface;
9+
plugins: ValidationPlugins;
10+
}
11+
712
export interface ValidateOptionsInterface {
813
showErrors?: boolean,
914
related?: boolean,
@@ -36,6 +41,7 @@ export interface ValidationPlugins {
3641
svk?: ValidationPlugin;
3742
yup?: ValidationPlugin;
3843
zod?: ValidationPlugin;
44+
joi?: ValidationPlugin;
3945
}
4046

4147
export type ValidationPackage = any;
@@ -75,4 +81,5 @@ export enum ValidationHooks {
7581
onError = 'onError',
7682
}
7783

84+
7885
export default ValidatorInterface;

src/validators/JOI.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import _ from "lodash";
2+
import {
3+
ValidationPlugin,
4+
ValidationPluginConfig,
5+
ValidationPluginConstructor,
6+
ValidationPluginInterface,
7+
} from "../models/ValidatorInterface";
8+
9+
class JOI implements ValidationPluginInterface {
10+
promises = [];
11+
12+
config = null;
13+
14+
state = null;
15+
16+
extend = null;
17+
18+
validator = null;
19+
20+
schema = null;
21+
22+
constructor({
23+
config,
24+
state = null,
25+
promises = [],
26+
}: ValidationPluginConstructor) {
27+
this.state = state;
28+
this.promises = promises;
29+
this.extend = config?.extend;
30+
this.validator = config.package;
31+
this.schema = config.schema;
32+
this.extendValidator();
33+
}
34+
35+
extendValidator(): void {
36+
// extend using "extend" callback
37+
if (typeof this.extend === "function") {
38+
this.extend({
39+
validator: this.validator,
40+
form: this.state.form,
41+
});
42+
}
43+
}
44+
45+
validate(field): void {
46+
const { error } = this.schema.validate(field.state.form.validatedValues, { abortEarly: false });
47+
if (!error) return;
48+
49+
const fieldPathArray = field.path.split('.');
50+
51+
const fieldErrors = error.details
52+
.filter(detail => {
53+
const errorPathString = detail.path.join('.');
54+
const fieldPathString = fieldPathArray.join('.');
55+
return errorPathString === fieldPathString || errorPathString.startsWith(`${fieldPathString}.`);
56+
})
57+
.map(detail => {
58+
// Replace the path in the error message with the custom label
59+
const label = detail.context?.label || detail.path.join('.');
60+
const message = detail.message.replace(`${detail.path.join('.')}`, label);
61+
return message;
62+
});
63+
64+
if (fieldErrors.length) {
65+
field.validationErrorStack = fieldErrors;
66+
}
67+
}
68+
}
69+
70+
export default (config?: ValidationPluginConfig): ValidationPlugin => ({
71+
class: JOI,
72+
config,
73+
});

tests/data/_.nested.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import $V4 from "./forms/nested/form.v4";
3333
import $Z from "./forms/nested/form.z";
3434
import $Z1 from "./forms/nested/form.z1";
3535
import $Z2 from "./forms/nested/form.z2";
36+
import $Z3 from "./forms/nested/form.z3";
37+
import $Z4 from "./forms/nested/form.z4";
3638
import $X from "./forms/nested/form.x";
3739

3840
export default {
@@ -68,5 +70,7 @@ export default {
6870
$Z,
6971
$Z1,
7072
$Z2,
73+
$Z3,
74+
$Z4,
7175
$X,
7276
};

tests/data/forms/nested/form.z3.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
import { expect } from "chai";
3+
import j from "joi";
4+
import FormInterface from "../../../../src/models/FormInterface";
5+
import OptionsModel from "../../../../src/models/OptionsModel";
6+
import { Form } from "../../../../src";
7+
import joi from "../../../../src/validators/JOI";
8+
import { ValidationPlugins } from "../../../../src/models/ValidatorInterface";
9+
10+
11+
const fields = [
12+
"user.username",
13+
"user.email",
14+
"user.password",
15+
"user.passwordConfirm",
16+
];
17+
18+
const values = {
19+
user: {
20+
username: 'a',
21+
email: 'notAValidEmail@',
22+
password: 'x',
23+
passwordConfirm: 'mysecretpassword',
24+
}
25+
}
26+
27+
const schema = j.object({
28+
user: j.object({
29+
username: j.string().min(3).required().label('Username'),
30+
email: j.string().email().required(),
31+
password: j.string().min(6).max(25).required(),
32+
passwordConfirm: j.string().min(6).max(25).valid(j.ref('password')).required().messages({
33+
'any.only': 'Passwords do not match',
34+
'any.required': 'Password confirmation is required',
35+
}),
36+
}).required()
37+
});
38+
39+
const plugins: ValidationPlugins = {
40+
joi: joi({
41+
package: j,
42+
schema,
43+
}),
44+
};
45+
46+
const options: OptionsModel = {
47+
validateOnInit: true,
48+
showErrorsOnInit: true,
49+
};
50+
51+
export default new Form({
52+
fields,
53+
values,
54+
}, {
55+
plugins,
56+
options,
57+
name: "Nested-Z3",
58+
hooks: {
59+
onInit(form: FormInterface) {
60+
describe("Check joi validation flag", () => {
61+
it('user.username hasError should be true', () => expect(form.$('user.username').hasError).to.be.true);
62+
it('user.email hasError should be true', () => expect(form.$('user.email').hasError).to.be.true);
63+
it('user.password hasError should be true', () => expect(form.$('user.password').hasError).to.be.true);
64+
it('user.passwordConfirm hasError should be true', () => expect(form.$('user.passwordConfirm').hasError).to.be.true);
65+
});
66+
67+
describe("Check joi validation errors", () => {
68+
it('user.username error should equal joi error', () => expect(form.$('user.username').error).to.be.equal('"Username" length must be at least 3 characters long'));
69+
it('user.email error should equal joi error', () => expect(form.$('user.email').error).to.be.equal('"user.email" must be a valid email'));
70+
it('user.password error should equal joi error', () => expect(form.$('user.password').error).to.be.equal('"user.password" length must be at least 6 characters long'));
71+
it('user.passwordConfirm error should equal joi error', () => expect(form.$('user.passwordConfirm').error).to.be.equal('Passwords do not match'));
72+
});
73+
74+
}
75+
}
76+
});

0 commit comments

Comments
 (0)