Skip to content

Commit

Permalink
feat: introduced JOI validator
Browse files Browse the repository at this point in the history
  • Loading branch information
foxhound87 committed Jul 28, 2024
1 parent 2d9f991 commit bfd463f
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 6.10.0 (master)

- Introduced `JOI` validation plugin and driver.
- Added `ValidatorConstructor` inferface

# 6.9.4 (master)

- Fix: #636
Expand Down
99 changes: 99 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"license": "MIT",
"version": "0.0.0-development",
"author": "Claudio Savino <[email protected]> (https://twitter.com/foxhound87)",
"description": "Automagically manage React forms state and automatic validation with MobX.",
"description": "Reactive MobX Form State Management",
"homepage": "https://github.com/foxhound87/mobx-react-form#readme",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down Expand Up @@ -95,6 +95,7 @@
"eslint": "^8.35.0",
"eslint-plugin-import": "^2.27.5",
"husky": "0.13.1",
"joi": "^17.13.3",
"json-loader": "0.5.4",
"lodash-webpack-plugin": "^0.11.6",
"mobx": "^6.3.3",
Expand Down
4 changes: 3 additions & 1 deletion src/Validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ValidatorInterface, {
ValidationPlugin,
ValidationPluginInterface,
ValidationPlugins,
ValidatorConstructor,
} from "./models/ValidatorInterface";
import { FormInterface } from "./models/FormInterface";
import { FieldInterface } from "./models/FieldInterface";
Expand All @@ -25,11 +26,12 @@ export default class Validator implements ValidatorInterface {
svk: undefined,
yup: undefined,
zod: undefined,
joi: undefined,
};

error: string | null = null;

constructor(obj: any = {}) {
constructor(obj: ValidatorConstructor) {
makeObservable(this, {
error: observable,
validate: action,
Expand Down
7 changes: 7 additions & 0 deletions src/models/ValidatorInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import {FieldInterface} from "./FieldInterface";
import {FormInterface} from "./FormInterface";
import {StateInterface} from "./StateInterface";

export interface ValidatorConstructor {
form: FormInterface;
plugins: ValidationPlugins;
}

export interface ValidateOptionsInterface {
showErrors?: boolean,
related?: boolean,
Expand Down Expand Up @@ -36,6 +41,7 @@ export interface ValidationPlugins {
svk?: ValidationPlugin;
yup?: ValidationPlugin;
zod?: ValidationPlugin;
joi?: ValidationPlugin;
}

export type ValidationPackage = any;
Expand Down Expand Up @@ -75,4 +81,5 @@ export enum ValidationHooks {
onError = 'onError',
}


export default ValidatorInterface;
73 changes: 73 additions & 0 deletions src/validators/JOI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import _ from "lodash";
import {
ValidationPlugin,
ValidationPluginConfig,
ValidationPluginConstructor,
ValidationPluginInterface,
} from "../models/ValidatorInterface";

class JOI implements ValidationPluginInterface {
promises = [];

config = null;

state = null;

extend = null;

validator = null;

schema = null;

constructor({
config,
state = null,
promises = [],
}: ValidationPluginConstructor) {
this.state = state;
this.promises = promises;
this.extend = config?.extend;
this.validator = config.package;
this.schema = config.schema;
this.extendValidator();
}

extendValidator(): void {
// extend using "extend" callback
if (typeof this.extend === "function") {
this.extend({
validator: this.validator,
form: this.state.form,
});
}
}

validate(field): void {
const { error } = this.schema.validate(field.state.form.validatedValues, { abortEarly: false });
if (!error) return;

const fieldPathArray = field.path.split('.');

const fieldErrors = error.details
.filter(detail => {
const errorPathString = detail.path.join('.');
const fieldPathString = fieldPathArray.join('.');
return errorPathString === fieldPathString || errorPathString.startsWith(`${fieldPathString}.`);
})
.map(detail => {
// Replace the path in the error message with the custom label
const label = detail.context?.label || detail.path.join('.');
const message = detail.message.replace(`${detail.path.join('.')}`, label);
return message;
});

if (fieldErrors.length) {
field.validationErrorStack = fieldErrors;
}
}
}

export default (config?: ValidationPluginConfig): ValidationPlugin => ({
class: JOI,
config,
});
4 changes: 4 additions & 0 deletions tests/data/_.nested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import $V4 from "./forms/nested/form.v4";
import $Z from "./forms/nested/form.z";
import $Z1 from "./forms/nested/form.z1";
import $Z2 from "./forms/nested/form.z2";
import $Z3 from "./forms/nested/form.z3";
import $Z4 from "./forms/nested/form.z4";
import $X from "./forms/nested/form.x";

export default {
Expand Down Expand Up @@ -68,5 +70,7 @@ export default {
$Z,
$Z1,
$Z2,
$Z3,
$Z4,
$X,
};
76 changes: 76 additions & 0 deletions tests/data/forms/nested/form.z3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable import/no-extraneous-dependencies */
import { expect } from "chai";
import j from "joi";
import FormInterface from "../../../../src/models/FormInterface";
import OptionsModel from "../../../../src/models/OptionsModel";
import { Form } from "../../../../src";
import joi from "../../../../src/validators/JOI";
import { ValidationPlugins } from "../../../../src/models/ValidatorInterface";


const fields = [
"user.username",
"user.email",
"user.password",
"user.passwordConfirm",
];

const values = {
user: {
username: 'a',
email: 'notAValidEmail@',
password: 'x',
passwordConfirm: 'mysecretpassword',
}
}

const schema = j.object({
user: j.object({
username: j.string().min(3).required().label('Username'),
email: j.string().email().required(),
password: j.string().min(6).max(25).required(),
passwordConfirm: j.string().min(6).max(25).valid(j.ref('password')).required().messages({
'any.only': 'Passwords do not match',
'any.required': 'Password confirmation is required',
}),
}).required()
});

const plugins: ValidationPlugins = {
joi: joi({
package: j,
schema,
}),
};

const options: OptionsModel = {
validateOnInit: true,
showErrorsOnInit: true,
};

export default new Form({
fields,
values,
}, {
plugins,
options,
name: "Nested-Z3",
hooks: {
onInit(form: FormInterface) {
describe("Check joi validation flag", () => {
it('user.username hasError should be true', () => expect(form.$('user.username').hasError).to.be.true);
it('user.email hasError should be true', () => expect(form.$('user.email').hasError).to.be.true);
it('user.password hasError should be true', () => expect(form.$('user.password').hasError).to.be.true);
it('user.passwordConfirm hasError should be true', () => expect(form.$('user.passwordConfirm').hasError).to.be.true);
});

describe("Check joi validation errors", () => {
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'));
it('user.email error should equal joi error', () => expect(form.$('user.email').error).to.be.equal('"user.email" must be a valid email'));
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'));
it('user.passwordConfirm error should equal joi error', () => expect(form.$('user.passwordConfirm').error).to.be.equal('Passwords do not match'));
});

}
}
});
Loading

0 comments on commit bfd463f

Please sign in to comment.