npm package: @ngserveio/you-good
Description: A client and backend agnostic validation library created by [NgServe.io](https://ngserve.io).
Author: Paul Mead
Latest version: 10.0.0

# @ngserveio/you-good

A client and backend agnostic validation library created by [NgServe.io](https://ngserve.io).

Read More at [Angular Tutorial - Sharing Validations Between Angular and NestJs](https://ngserve.io/angular-tutorial-sharing-validations/)

## Running unit tests

Run `nx test shared-validator` to execute the unit tests via [Jest](https://jestjs.io).

## Purpose

This library provides the ability to validate models with the consuming service defining validations for the model provided. It is meant to help bridge the gap more easily between back and front end code as models on both sides need validation in a reusable way.

### Types

| Name                          | Returns                                                                        | Description                                                                                                                                                     |
| ----------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ValidationErrors`            | `{ [validationName: string]: unknown }`                                        | The validation type and the values associated with that validation e.g. `{ required: true }`                                                                    |
| `PropertyValidationErrors<T>` | `Record<keyof T, unknown>`                                                     | Provides a key on property `T` of `unknown` type which will be the return validation. e.g. `{firstName: { required: true, maxLength: { max: 50, actual: 51 } }` |
| `ValidatorFn<T>`              | `ValidationErrors`                                                             | A function that returns the validations per the validator on a property                                                                                         |
| `ValidationField<T>`          | `(item: Partial<T>) => unknown`                                                | A delegate method that returns the value to be validated. e.g. `(item: { name: string }) => item.name`                                                          |
| `PropertyValidators<T>`       | `Record<keyof T, ValidatiorFn<T>>[]`                                           | A list of validators of a particular property on `T` e.g. `{ firstName: [ required, max(10) ], email: { email, maxLength(50) } }`                               |
| `Validator<T>`                | `(value: ValidationField<T>) => ValidatorFn<T>`                                | A validator that chooses the value in which to validate per the delegate `ValidationField<T>`.                                                                  |
| `ModelValiatorFn<T>`          | `(item: Partial<T>, partial?: boolean) => PropertyValidationErrors<T> or Null` | A validate function that can choose to do partial validations depend on what's the model returns                                                                |

### Validators

Validation Errors refers to the `ValidationErrors` return when the validation is executed.

| Validator                                                      | Validation Errors                                           | Description                                                                                            |
| -------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `keys<T>(fn: ValidationField<T>): ValidatorFn<T>`              | `{keys: false}`                                             | Checks if the item being validated has any keys e.g. `Object.keys({name: 'Steve'}).length > 0`         |
| `required<T>(fn: ValidationField<T>): ValidatorFn<T>`          | `{ required: true}`                                         | Checks the value is not null or undefined and is not empty or white space.                             |
| `emptyOrWhiteSpace<T>(fn: ValidationField<T>): ValidatorFn<T>` | `{ emptyOrWhiteSpace: true }`                               | Checks if the provided value is empty or white space                                                   |
| `lessThan(compareValue: number): Validator<T>`                 | `{ lessThan: { compareValue: number, actual: number } }`    | Checks if the `itemValue` is greater than the `compareValue` supplied in the validation.               |
| `greaterThan(compareValue: number): Validator<T>`              | `{ greaterThan: { compareValue: number, actual: number } }` | Checks if the `itemValue` is less than the `compareValue` supplied in the validation.                  |
| `email<T>(fn: ValidationField<T>): ValidationFn<T>`            | `{ email: false }`                                          | Checks if the provided value is an email address.                                                      |
| `url<T>(fn: ValidationField<T>): ValidatorFn<T>`               | `{url: false}`                                              | Checks if the value provided is a URL                                                                  |
| `minLength<T>(number): Validator<T>`                           | `{ minLength: { compareValue: number, actual: number } }`   | Checks the min length required of the `compareValue` with `itemValue` for a property in the validation |
| `pattern<T>(pattern: string): ValidatorFn<T>`                  | `{ pattern: { regEx: string, value: StringOrNumber } }`     | Checks the pattern of the supplied field value.                                                        |
| `maxLength<T>(compareValue: number)`                           | `{ maxLength: { compreValue: number, actual: number } }`    | Checks the string value length being supplied isn't over the `compareValue`                            |

## Validating

One `validate<T>` method exists to help validate a model.

| Method                                                                                                          | Description                                                                                                      |
| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `validate<T>(validatorFns: PropertyValidators<T>): (item: Partial<T>) => ModelValidatorFn<T>`                   | Returns a delegate method for a model to be validated against. Validates partial for patch updates.<sup>\*</sup> |
| `propertyValidators<T>(fn: ValidationField<T>, fns:(validationField: ValidationField<T>) => ValidatorFn<T>)[])` | A helper method that has the `validationField<T>` supplied and an array of `ValidatorFns<T>`                     |

- Keep in mind if the object your checking in your `validatorFns` doesn't contain one of the properties on a partial check this will cause an error, so be careful with partial checks.

#### How to Use `validate`

```ts
type ModelT = {
  firstName: string;
  email: string;
  url: string;
  jerseyNumber: number | null;
};

const model: ModelT = {
  firstName: 'Steve',
  email: 's@s.com',
  url: 'https://detroitredwings.com',
  jerseyNumber: null,
};

const modelTValidator: (item: T) => PropertyValidationErrors<T> =
  validate<ModelT>({
    firstName: propertyValidators((p) => p.firstName, [required]),
    email: propertyValidators((p) => p.email, [required, email]),
    url: propertyValidators((p) => p.url, [url]),
    jerseyNumber: propertyValidators((p) => p.jerseyNumber, [required]),
  });

modelTValidator(model); // { jerseyNumber: { required: true } }
```

### Sample Validators

Reference below as to how to write a custom validator.

```ts
// This is a Validator<T>
export function emptyOrWhiteSpace<T>(
  value: ValidationField<T>
): ValidatorFn<T> {
  return (item: T): ValidationErrors => {
    const itemValue = value(item);

    return isEmptyOrWhiteSpace(itemValue as string)
      ? ({ emptyOrWhiteSpace: true } as Record<string, unknown>)
      : null;
  };
}

// Returns a Validator<T>
export function lessThan<T>(compareValue: number): Validator<T> {
  return (value: ValidationField<T>): ValidatorFn<T> => {
    return (item: T): ValidationErrors => {
      const itemValue = value(item) as number;
      return itemValue > compareValue
        ? ({ lessThan: { compareValue, actual: itemValue } } as Record<
            string,
            unknown
          >)
        : null;
    };
  };
}
```

## FluidValidator<T>

Fluid validations provides an easier way to validate a model in a fluid way. It provides customizations for nested validations for nested properties.

```ts
// Sample Types

type ProductAttribute = {
  id: string;
  productId: string;
};

type ProductImage = {
  id: string;
  productId: string;
  altText: string;
};

type Product = {
  id: string;
  name: string;
  attributes: Nullable<ProductAttribute[]>;
  image: ProductImage;
  price: number;
  email: string;
  url: string;
};

const productImageValidator = new FluidValidator<ProductImage>()
  .prop('id') // Maps to a particular property in Product Image
  .maxLength(36) // Standard Validations listed below
  .prop('productId')
  .required();

const productAttributeValidator = new FluidValidator<ProductAttribute>()
  .prop('id')
  .maxLength(36)
  .prop('productId')
  .required();

const productValidator = new FluidValidator<Product>()
  .prop('id')
  .maxLength(36)
  .prop('name')
  .emptyOrWhiteSpace()
  .required()
  .minLength(1)
  .maxLength(50)
  .prop('image')
  .required()
  .custom(({ image }) => {
    // custom validations  using another validator
    return productImageValidator.validate(image);
  })
  .prop('price')
  .greaterThan(0)
  .prop('email')
  .email()
  .prop('url')
  .url()
  .pattern(/^https:\/\//)
  .prop('attributes')
  .required()
  .custom(({ attributes }) => {
    const validationMessages: ValidationErrors = attributes.reduce(
      (errors, attribute, index) => {
        const validationErrors = productAttributeValidator.validate(attribute);
        if (validationErrors) {
          errors[index] = validationErrors;
        }

        return errors;
      },
      {}
    );

    return hasKeys(validationMessages) ? validationMessages : null;
  });
```

The `prop('propertyName')` method scopes the validations to the property of the model. The `propertyName` must be a key of the model being validated.

### Methods

| Name                                                                                | Description                                                                                                    |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `prop(propName: string): FluidValidator<T>`                                         | Defines the property being validated                                                                           |
| `required(): FluidValidator<T>`                                                     | Defines the property is required.                                                                              |
| `maxLength(length: number): FluidValidator<T>`                                      | The max length of a string value                                                                               |
| `minLength(length: number): FluidValidator<T>`                                      | The min length of a string value                                                                               |
| `lessThan(compare: number): FluidValidator<T>`                                      | Compares the property must be less than the compare value                                                      |
| `greaterThan(compare: number): FluidValidator<T>`                                   | Compres the property must be greater than the compare value                                                    |
| `emtpyOrWhiteSpace(): FluidValidator<T>`                                            | Checks the value is not empty string or white space.                                                           |
| `email(): FluidValidator<T>`                                                        | Checks the property is an email address                                                                        |
| `url(): FluidValidator<T>`                                                          | Checks the property is a url                                                                                   |
| `pattern(regEx: RegExp): FluidValidator<T>`                                         | Checks the property is a regular expression                                                                    |
| `custom(func: Func<T                                                                | Partial<T>, Nullable<ValidationErrors>>, partial: boolean): FluidValidator<T>`<sup style="color: red">\*</sup> | Provides the ability to write a custom validation function or reuse another `FluidValidator<T>` such as a nested object. |
| `validate(item:T): Nullable<Partial<PropertyValidationErrors<T>>>`                  | Validates and full object for all the properties defined in the validator                                      |
| `validatePartial(item: Partial<T>): Nullable<Partial<PropertyValidationErrors<T>>>` | Validates a partial object of the properties provided                                                          |
| `extends<T & R>(): FluidValidator<T & R>`                                           | Extends the available properties to validate.                                                                  |
| `omit<K extends KeyOfType<T>>(keys: K[]): FluidValidator<Omit<T, K>>`               | Omits certain properties from being validated.                                                                 |
| `pick<K extends KeyOfType<T>>(keys: K[]): FluidValidator<Pick<T, K>>`               | Picks certain properties to be validated.                                                                      |

<sup style="color: red">\*</sup> The `custom` function may provide a `Partial<T>` so all properties may not be available for validating and you'll want to code if function is a partial check. The assumption is that if you're doing a partial check the object was previously validated in full.

The `custom` function accepts a `partial` to determine if the validation is a partial validation. This matters when you want to check multiple multiple and dependent properties of the item.