@bavaria2tm/validator
TypeScript icon, indicating that this package has built-in type declarations

1.0.2 • Public • Published

Validators

Description

The validation of data is an essential part of an application, used in api endpoints, input data validation, ... just to name a few examples. Most commonly in nodejs the express validators, joi validators, or class validators are used.

In our case we needed something with type save validation similar to class validators but combined with the method chaining and flexibility of joi validators.Furthermore we need to use the validators in frontend and backend and have to be therefor independent of a library. Because of all of these reasons we decided to create our on validators.

Links

What is a Class Schema in our Use Case?

A class schema is a TypeScript class that serves as a blueprint for the data we send or receive between different parts of our application, such as between the client and server. Beyond just describing the shape of the data, our Validators also have built-in rules for validating and transforming this data. This ensures that the data is not only correctly formatted but also meets specific conditions before we process it further. Think of it as a smart data package that knows how to check itself for errors and even correct some of them automatically.

Usage as Class Schema's

The advantages of Class Schemas are that they have strict type safety and are cleaner then the object validators in highting in the IDE (class definitions pop out more then constants)

1. Define a Schema Class

class UserSchema extends ValidatorSchema<IUser> {
  //0 - validate the name, which is MANDATORY
  // > Note: by default all fields are "REQUIRED"
  name = Validator.String()
    .isNotEmpty()
    .minLength(5)
    .hasNoSpace() // example of VALIDATORS
    .trim()
    .toLowerCase()
    .encodeBase64(); // example of TRANSFORMERS

  //1 - OPTIONAL field
  // > Note: alternatively use ".optionalIf((o: IUser) => ...)"
  nickName = Validator.String().optional();

  //2 - set DEFAULT value
  age = Validator.Number().isPositive().default(-1);

  //3 - conditional "IF" validation
  adult = Validator.Boolean().validateIf((value: boolean) => adult);

  //4 - validate an ARRAY, of strings
  friends = Validator.Array()
    .isNotEmpty()
    .maxLength(10)
    .shuffle()
    .each(Validator.String().trim());

  //5 - validate an OBJECT, external class schema
  contact = Validator.Object().hasNoNestedObjects().inspect(ContactSchema); // nested object of "IAddress"

  //7 - validate an ARRAY, of OBJECTS
  friendContacts = Validator.Array().isNotEmpty().inspectEach(ContactSchema);
}

class ContactSchema extends ValidatorSchema<IContact> {
  phone = Validator.String().isNotEmpty();
  email = Validator.String().isNotEmpty().isEmail();
}

2. Validate By Class Schema

Use the ValidatorSchema that was created before to validate the data with one of the below approaches.

//0 - validate by boolean
const isValid: boolean = Validator.Schema.isValid(data, UserSchema);
if (isValid) console.log(" > user is valid");
else console.log(" > user is invalid");

//1 - validate with getting the detailed infos
// > interface IValidatorError {
// >   property : string;
// >   value    : boolean;
// >   errors   : string[];
// >   children : IValidatorError[];
// > }
const validationErrors: IValidatorError[] = Validator.Schema.validate(
  data,
  UserSchema
);
console.log(" > validationErrors: ", validationErrors);

Note: the transformers are also executed during the validation

3. Process By Class Schema

Use the UserSchema that was created before to process / clean the data based on the transformers.

// process the user data by the schema
// > Note: this will execute logic like trim values or apply default values,
// > and more. It will return THROW an ERROR for an invalid object.
const userData: IUser = Validator.Schema.transform(requestBodyData, UserSchema);

Note: the validation is also executed during the processing, so if the data is invalid we get the error

Usage of Object Schemas

The advantages of schemas are, that they give high type safety (it forces us to have a validator for each value and forces the exact validator type). The disadvantage is we can only have a single validation chain for each value.

Note: use the Schema validation for configs and other partial inline validations

1. Define a Object Schema

const userSchema: ValidatorSchema<IUser> = {
  //0 - simple validators
  name: Validator.String().isNotEmpty().trim().toLowerCase(),
  nickName: Validator.String().optional(),

  //1 - validate an ARRAY, of strings
  friends: Validator.Array()
    .each(Validator.String().trim())
    .isNotEmpty()
    .maxLength(10)
    .shuffle(),

  //2 - validate an OBJECT, as INLINE OBJECT
  address: Validator.Object()
    .inspect({
      phone: Validator.String().toLowerCase(),
      email: Validator.String().toLowerCase()
    })
    .hasNoNestedObjects(),

  //3 - validate an OBJECT, as INLINE
  contact: {
    phone: Validator.String().toLowerCase(),
    email: Validator.String().toLowerCase()
  }
};

Note: the schemas can have inline objects / nested validators, but you can not have a child schema

2. Validate By Object Schema

Use the Schema that was created before to validate the data with one of the below approaches.

//0 - validate by boolean
const isDataValid: boolean = Validator.Schema.isValid(data, userSchema);
if (isDataValid) console.log(" > user is valid");
else console.log(" > user is invalid");

//1 - validate with getting the detailed infos
const validationErrors: IValidatorError[] = Validator.Schema.validate(
  data,
  userSchema
);
console.log(" > validationErrors: ", validationErrors);

//2 - validate inline
const validationErrors: IValidatorError[] = Validator.Schema.validate(data, {
  name: Validator.String().isNotEmpty().trim().toLowerCase(),
  nickName: Validator.String().optional()
  // ... add other validators
});

Note: the transformers are also executed during the validation

3. Process By Object Schema

Use the objectSchema that was created before to process / clean the data based on the transformers.

//0 - process the user data by the schema
// > Note: this will execute logic like trim values or apply default values,
// > and more. It will return THROW an ERROR for an invalid object.
const userData: IUser = Validator.Schema.transform(requestBodyData, userSchema);

//1 - transform inline
const validationErrors: IValidatorError[] = Validator.Schema.transform(data, {
  name: Validator.String().isNotEmpty().trim().toLowerCase(),
  nickName: Validator.String().optional()
  // ... add other validators
});

Note: the validation is also executed during the processing, so if the data is invalid we get the error

Other Usages

1. Direct Execution

The validators can also be executed directly

//0 - validate
const isValid: boolean = Validator.String()
  .isNotEmpty()
  .isLowerCase()
  .isValid("My Cat");

//1 - processing value
const processedValue: string = Validator.String()
  .isNotEmpty()
  .toLowerCase()
  .process("My Cat");

2. Use Validators for Angular FormControl

The validators can even be used with Angular FormGroup / FormControls and can provide the following standard required by angular

// standard required by angular
export function passwordValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const password: string = control.value; // ... check "control.value"
    return this.isValidPassword(password)
      ? { weekPassword: "Password is week" }
      : null;
  };
}

To convert the validator into a FormControls Validator use the approach below

//0 - get the validator as an wrapped "angular validator"
// > Note: returns "(control: AbstractControl): ValidationErrors | null"
const angularValidator: ValidatorFn = Validator.String().isPassword().ngForm();

//1 - use the validator
const signupForm: FormGroup<ILoginForm> = new FormGroup<ILoginForm>({
  password: new FormControl<string>("", angularValidator),
  name: new FormControl<string>("", Validator.String().isNotEmpty().ngForm()) // use it directly inline
  // ...
});

Appendix

Appendix A: Validation Result

The validation returns the following result. Be aware that arrays and objects can have children.

interface IValidatorError {
  property: string;
  value: boolean;
  errors: string[];
  children: IValidatorError[];
}

If the following example data is validated, it would give the following result below.

// data which has invalid entries
const data = {
  name: "Yoda",
  age: -1, // negative number is invalid
  hobbies: ["swimming", undefined, "running"], // undefined is invalid
  address: {
    country: null, // null is not a valid country
    zipCode: 45678
  }
};

// validation result
const validationResult: Partial<IValidatorError>[] = [
  {
    property: "age",
    value: -1,
    errors: ["No negative value"],
    children: []
  },
  {
    property: "hobbies",
    value: ["swimming", undefined, 22, "running"],
    errors: ["Some items are invalid"],
    children: [
      {
        property: "1", // index in the array
        value: undefined,
        errors: ["Item is undefined"],
        children: []
      }
    ]
  },
  {
    property: "address",
    value: { country: undefined, zipCode: 45678 },
    errors: ["Object is invalid"],
    children: [
      {
        property: "country", // property of the object
        value: null,
        errors: ["Has to be a valid string"],
        children: []
      }
    ]
  }
];

Appendix B: Advanced Usage

The below example is advanced and can be used as following: lets assume the frontend is sending a user's password in an encrypted format, then the below validator would be executed as following.

  1. Check if we got any string value isNotEmpty()

  2. Decrypt the received password decryptASE128('9sdsdafsdafafh8asdsdafsdaffh9h89')

  3. Validate if the password is secure enough isPassword()

  4. Hash the password, as we only need the hash to compare it transform((value: string) => Util.Hash(value))

class UserSchema extends ValidatorSchema<IUser> {
  password = Validator.String()
    .isNotEmpty()
    .decryptASE128("9sdsdafsdafafh8asdsdafsdaffh9h89")
    .isPassword()
    .map((value: string) => Util.Hash(value));
}

Appendix C: Validator Utility

The Validator Utility is a handy tool that offers simple and effective validation for various entities such as emails, domains, passwords, etc. It provides two core functions for handling validation tasks, allowing you to either simply check validity or obtain the specific reasons for validation failures.

Each validator has the following two methods

  1. isValid(entity: string): boolean, use this function when you only need to know if an entity is valid or not. The function returns a boolean value.

    if (Validator.Email.isValid(email)) {
      console.log("Email is valid!");
    } else {
      console.log("Email is invalid!");
    }
  2. validate(entity: string): string[], use this function when you need to know the reasons for validation failure, such as when you need to display specific feedback to the user. The function returns an array of strings that describe the validation errors.

    //0 - validate the mail
    const errors: string[] = Validator.Email.validate(email);
    
    //1 - show the errors
    if (errors.length === 0) {
      console.log("Email is valid!");
    } else {
      console.log("Email is invalid for the following reasons: ", errors);
    }

Package Sidebar

Install

npm i @bavaria2tm/validator

Weekly Downloads

3

Version

1.0.2

License

MIT

Unpacked Size

616 kB

Total Files

26

Last publish

Collaborators

  • bavaria.2tm