# class-utils

This package contains all `class-validator` and `class-tranformer` functions with custom class decorators that can be used to validate a class.

## Usage

```javascript
import { IsString } from '@apizr/class-utils/validators';
import { Transform } from '@apizr/class-utils/transformers';

class MyClass {
  @IsString()
  myString: string;

  @Transform((value) => value === 'true')
  myBoolean: boolean;
}
```

## Dependencies

This package depends on `jsonpath-plus` to be able to use some decorators (e.g. `ValidateSubKeyExistIf`).
See [documentation](https://www.npmjs.com/package/jsonpath-plus) for more information.

## class-validator decorators and class-transformer

See [documentation](https://github.com/typestack/class-validator/blob/develop/README.md) of `class-validator` for the list of default exports.
See [documentation](https://github.com/typestack/class-transformer/blob/develop/README.md) of `class-transformer` for the list of default export.

## Custom transformers

### SplitString

`SplitString(separator: string = ',')`

Will transform a string received to an array of string split by separator.

#### Usage

```javascript
import { SplitString } from '@apizr/class-utils/tranformers';

class MyStringClass {
  @SplitString()
  myString: string[];
}
```

## Custom decorators

## IsGreater

`IsGreater(compareTo: string | number, options?: { allowEqual?: boolean })`

Checks if the number is greater (or equal) than another property of the class or a specified value.

#### Usage

```typescript
import { IsGreater } from '@apizr/class-utils/validators';

// Compare with other property
class ExampleClass {
  @IsGreater('myOtherNumber')
  myNumber: number;

  myOtherNumber: number;
}

// Compare with a fixed number
class ExampleFixed {
  @IsGreater(5)
  value: number;
}

// Allow equal values as well
class ExampleAllowEqual {
  @IsGreater('minValue', { allowEqual: true })
  value: number;

  minValue: number;
}
```

- When using a property name as the first argument, it compares the decorated property with that property in the same object.
- The optional `allowEqual` flag allows values to be equal and pass validation.

## IsLower

`IsLower(compareTo: string | number, options?: { allowEqual?: boolean })`

Checks if the number is lower than another property of the class or a specified value.

#### Usage

```typescript
import { IsLower } from '@apizr/class-utils/validators';

// Compare with other property
class ExampleClass {
  @IsLower('myOtherNumber')
  myNumber: number;

  myOtherNumber: number;
}

// Compare with a fixed number
class ExampleFixed {
  @IsLower(10)
  value: number;
}

// Allow equal values as well
class ExampleAllowEqual {
  @IsLower('maxValue', { allowEqual: true })
  value: number;

  maxValue: number;
}
```

- When using a property name as the first argument, it compares the decorated property with that property in the same object.
- The optional `allowEqual` flag allows values to be equal and pass validation.

### IsBeforeDate

`IsBeforeDate(property: string, options: ValidationOptions)`

Checks if the date is before the date present in the same objet at the specified key.

#### Usage

```javascript
import { IsBeforeDate } from '@apizr/class-utils/validators';

class Dates {
  @IsBeforeDate('end')
  start: Date;

  end: Date;
}
```

If the other date is optional, you can specify a flag to prevent an error if no date is present

```javascript
import { IsBeforeDate } from '@apizr/class-utils/validators';

class Dates {
  @IsBeforeDate('end', { context: { otherDateIsOptional: true } })
  start: Date;

  end?: Date;
}
```

You can also use a function to get the date to compare with:

```javascript
import { IsBeforeDate } from '@apizr/class-utils/validators';

class Dates {
  @IsBeforeDate(() => new Date(Date.now() + 14_400_000))
  end: Date;
}
```

### IsAfterDate

`IsAfterDate(property: string, options: ValidationOptions)`

Checks if the date is after the date present in the same objet at the specified key.

#### Usage

```javascript
import { IsAfterDate } from '@apizr/class-utils/validators';

class Dates {
  start: Date;

  @IsAfterDate('start')
  end: Date;
}
```

If the other date is optional, you can specify a flag to prevent an error if no date is present

```javascript
import { IsAfterDate } from '@apizr/class-utils/validators';

class Dates {
  start?: Date;

  @IsAfterDate('start', { context: { otherDateIsOptional: true } })
  end: Date;
}
```

You can also use a function to get the date to compare with:

```javascript
import { IsAfterDate } from '@apizr/class-utils/validators';

class Dates {
  @IsAfterDate(() => new Date(Date.now() - 14_400_000))
  end: Date;
}
```

### IsFutureDate

`IsFutureDate(void)`

Checks if the date is in the future.

#### Usage

```javascript
import { IsFutureDate } from '@apizr/class-utils/validators';

class Date {
  @IsFutureDate()
  end: Date;
}
```

### StartsWith

`StartsWith(startValue: string | string[])`

Checks if the string starts with the specified value

#### Usage

```javascript
import { StartsWith } from '@apizr/class-utils/validators';

class Order {
  @StartsWith('PROD-')
  id: string;
}
```

### OneOfKeysIsDefined

`OneOfKeysIsDefined(keysToValidate: string[])`

Checks if at least one of the key has value (not `null` or `undefined`).

#### Usage

```javascript
import { OneOfKeysIsDefined } from '@apizr/class-utils/validators';

class Product {
  id: string;
  description?: string;
  name?: string;
}

class Order {
  @ValidateNested({ each: true })
  @Type(() => Product)
  @OneOfKeysIsDefined(['description', 'name'], { each: true })
  products: Product[];
}
```

### ValidateSort

`ValidateSort(allowedSort: string | string[])`

Checks that the string array contains valid keys with valid sorting directions (`asc` or `desc`).

#### usage

```javascript
import { ValidateSort } from '@apizr/class-utils/validators';

class Filters {
  @ValidateSort(['date.creation', 'date.modification'])
  sort: string[];
}
```

This will allow `date.creation:asc` or `date.modification:desc` for example.

### ValidateAtLeastOneKeyGroupExists

`ValidateAtLeastOneKeyGroupExists(groups: string[][])`

Checks if one of the keys groups exist in class.

#### Usage

```javascript
@ValidateAtLeastOneKeyGroupExists([
  ['key1', 'key2'],
  ['key2', 'key3']
])
```

Will validate that (`key1` and `key2`) or (`key2` and `key3`) exists in the class.

#### Usage

```javascript
import { ValidateAtLeastOneKeyGroupExists } from '@apizr/class-utils/validators';

@ValidateAtLeastOneKeyGroupExists([
  ['email', 'birthday'],
  ['firstname', 'lastname'],
])
class Customer {
  email: string;

  firstname: string;

  lastname: string;

  birthday: string;
}
```

### ValidateOnlyOneKeyGroupExists

`ValidateOnlyOneKeyGroupExists(groups: string[][])`

Checks if keys of a key groups exist in class.

#### Usage

```javascript
@ValidateOneKeyGroupExists([
  ['key1', 'key2'],
  ['key2', 'key3']
])
```

Will validate that (`key1` and `key2`) xor (exclusive or) (`key2` and `key3`) exists in the class.

#### Usage

```javascript
import { ValidateOnlyOneKeyGroupExists } from '@apizr/class-utils/validators';

@ValidateOnlyOneKeyGroupExists([
  ['email', 'birthday'],
  ['firstname', 'lastname'],
])
class Customer {
  email: string;

  firstname: string;

  lastname: string;

  birthday: string;
}
```

### ValidateSubKeyExistIf

`ValidateSubKeyExistIf(path: string, condition: (value: any) => boolean)`

Allows validating the presence of a specified key in a child object based on a given condition.
The child value described by the path option must have the @IsOptional() decorator (see example below).

#### Usage

```typescript
import { ValidateSubKeyExistIf } from '@apizr/class-utils/validators';

class ChildClass {
  @IsOptional() // Mandatory when this decorator is used
  targetedValue?: any;
}

@ValidateSubKeyExistIf(
  'child.targetedValue',
  (o) => o.conditionalValue !== false,
)
class ParentClass {
  @IsString()
  conditionalValue: boolean;

  @ValidateNested({ each: true })
  @Type(() => ChildClass)
  child: ChildClass;
}
```

You can also use `jsonpath-plus` to validate a key in an array of objects:

```typescript
import { ValidateSubKeyExistIf } from '@apizr/class-utils/validators';

class LittleChildClass {
  @IsOptional() // Mandatory when this decorator is used
  targetedValue?: string;
}

class ChildClass {
  @IsOptional()
  @ValidateNested({ each: true })
  @Type(() => LittleChildClass)
  myOtherValue?: LittleChildClass[];
}

@ValidateSubKeyExistIf(
  'child.myOtherValue[*].targetedValue',
  (o) => o.conditionalValue !== false,
)
class ParentClass {
  conditionalValue: boolean;
  @ValidateNested({ each: true })
  @Type(() => ChildClass)
  child: ChildClass;
}
```

### IsRecord

`IsRecord(allowedRecordType: AllowedType | AllowedType[], options: IsRecordValidateOptions)`

Check if the record is compliant to specified definition.

By default, the key type will be checked to be a string.

#### Usage

```typescript
import { IsRecord } from '@apizr/class-utils/validators';

class ChildClass {
  @IsString()
  key: string;
}

class ExampleClass {
  @IsRecord('boolean')
  record: Record<string, boolean>;

  @IsRecord(['string', 'undefined'])
  record: Record<string, string | undefined>;

  @IsRecord(ChildClass)
  record: Record<string, ChildClass>;

  @IsRecord('string', { key: 'number' })
  record: Record<number, string>;

  @IsRecord(ChildClass, { each: true })
  records: Record<string, ChildClass>[];

  @IsRecord([ChildClass, ChildClass2, 'undefined'])
  record: Record<string, ChildClass | ChildClass2 | undefined>;

  @IsRecord([{ type: ChildClass, array: true }, ChildClass2])
  record: Record<string, ChildClass[] | ChildClass2>;

  @IsRecord([{ type: [ChildClass, ChildClass2], array: true }, ChildClass3])
  record: Record<string, (ChildClass | ChildClass2)[] | ChildClass3>;

  @IsRecord({ key: 'string', value: { type: 'string', array: true } })
  record: Record<string, Record<string, string[]>>;
}
```

### MustNotExistIf

`MustNotExistIf(condition: (value: any) => boolean, jsonpath?: string, options?: ValidationOptions)`

Check if the value must not exist if the condition is met.

#### Usage

```typescript
import { MustNotExistIf } from '@apizr/class-utils/validators';

class User {
  @MustNotExistIf((o) => o.isGuest)
  password: string;

  isGuest: boolean;
}

const user = new User();
user.isGuest = true;
user.password = 'securePassword'; // Validation will fail because `isGuest` is true.
```

You can also use jsonpath-plus to validate a nested key:

```typescript
import { MustNotExistIf } from '@apizr/class-utils/validators';

class UserProfile {
  @MustNotExistIf(
    (jsonpathValue) =>
      Array.isArray(jsonpathValue) && jsonpathValue.includes('restricted'),
    '$.roles[*]',
  )
  adminSettings: string;

  roles: string[];
}

const profile = new UserProfile();
profile.roles = ['user', 'restricted'];
profile.adminSettings = 'Some Settings'; // Validation will fail because "roles" contains "restricted".
```

Default error message is `Property {{property}} must not exist if {{condition}} is true` but you can override it by passing a `message` option in the `ValidationOptions` object.

### IsUnion

`IsUnion(allowedUnionType: AllowedType[], options: IsUnionValidateOptions)`

Check if the union is compliant to specified definition.

#### Usage

```typescript
import { IsUnion } from '@apizr/class-utils/validators';

class ChildClass {
  @IsString()
  key: string;
}

class ExampleClass {
  @IsUnion(['string', 'undefined'])
  union: string | undefined;

  @IsUnion(['string', 'number'], { each: true })
  union: (string | number)[];

  @IsUnion([ChildClass, ChildClass2, 'undefined'])
  union: ChildClass | ChildClass2 | undefined;

  @IsUnion([{ type: ChildClass, array: true }, ChildClass2])
  union: ChildClass[] | ChildClass2;

  @IsUnion([{ type: [ChildClass, ChildClass2], array: true }, ChildClass3])
  union: (ChildClass | ChildClass2)[] | ChildClass3;

  @IsUnion([
    {
      type: [
        ChildClass,
        {
          key: 'string',
          value: { type: ChildClass2, array: true },
        },
      ],
      array: true,
    },
    ChildClass3,
  ])
  union: (ChildClass | Record<string, ChildClass2[]>)[] | ChildClass3;
}
```

### IsCron

`IsCron(inputOptions?: InputOptions['override'])`

Checks if the string is cron expression valid

See [documentation](https://github.com/Airfooox/cron-validate) of cron-validate lib to see the options.

#### Usage

```javascript
import { IsCron } from '@apizr/class-utils/validators';

class ExampleClass {
  @IsCron()
  cronExpression: string;

  @IsCron({
    useSeconds: true,
  })
  cronExpression: string;
}
```
