JSPM

@bavaria2tm/validator

1.0.2
    • ESM via JSPM
    • ES Module Entrypoint
    • Export Map
    • Keywords
    • License
    • Repository URL
    • TypeScript Types
    • README
    • Created
    • Published
    • Downloads 10
    • Score
      100M100P100Q43376F
    • License MIT

    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. This library is a lightweight, high-performance, easy-to-use, modular, extendable, and plugin-friendly validation library for typescript. It is designed to be fast, best-practices, security, reliable, robust, production-ready, cross-platform, unit-testing, integration-testing, continuous-integration, and build-tools.

    Package Exports

    • @bavaria2tm/validator
    • @bavaria2tm/validator/index.js

    This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@bavaria2tm/validator) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

    Readme

    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.

    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);
      }