2.0.0-0 • Published 4 years ago

at-valid v2.0.0-0

Weekly downloads
2
License
MIT
Repository
github
Last release
4 years ago

Logo of the project

@valid (at-valid)

Validate objects by assigning decorators/annotations to properties.

Validation of objects is tedious work. More often than not, validation code is (source-wise) far away from the objects you want to validate. So if you are changing an object, you have to search for the validation code in the whole project.

Imagine you could just use decorators directly on the properties or the whole class you want to validate!

Validation becomes as easy as:

class MyClass {
    @Required()
    @MinLength(3)
    name: string = "";
}

const validationError = await new DecoratorValidator().validate(myClassInstance);
if (validationError) {
    ...
}

TODO: link to the Angular FormBuilder adapter.

Installing / Getting started

Prerequisites

This package is implemented with ES2015 (see caniuse.com) in mind and thus should be compatible with even IE11.

Dependencies

Installation

NPM:

npm install --save at-valid

Yarn:

yarn add at-valid

Usage

Validation on classes/properties just needs decorators on the properties you want to validate.

We'll call these decorators used for validation "constraints".

Basic property validation

Add the desired decorators to the properties of your class (Decorating properties defined in the constructor is not supported (yet) due to limitations of typescript!).

Important facts:

  • By convention, all constraints treat null and undefined as valid values (in other words: optional values). To enforce a value, there is the @Required() decorator.
  • Properties are validated independently of each other.
  • All constraints per property are executed sequentially in the order they appear in the source (i.e.: top-down). This also holds for async constraints!. Constraint-execution for that property is stopped at the first error.

This makes declaring validation as easy as:

class MyClass {
    // some optional property that - if there is a value - needs a minimum length of 3
    @MinLength(3)
    optionalProperty?: string;
    
    // this property is required and must evaluate to a number with a value >= 5
    @Required()
    @IsNumber()
    @Min(5)
    someValueFromAPI?: any;
}

To perform the actual validation:

const fixture = new MyClass();

const result = await new DecoratorValidation().validate(fixture);

if (result.isError) {
    console.log('property validation errors: ', JSON.stringify(result.propertyErrors));   
}

// prints:
{
	"success": false,
	"propertyErrors": {
		"someValueFromAPI": {
			"propertyKey": "someValueFromAPI",
			"path": "$.someValueFromAPI",
			"validatorName": "Required",
			"validatorFnContext": {"args": {}, "customContext": {}}
		}
	}
}
// please note: there are no errors for optionalProperty since it is not required/optional

Various constraints are found in the src/decorators/constraints folder. Constraint names (for e.g. building error messages) are defined in src/decorators/ValidatorNames.ts.

To see how you might implement your own decorators, see Advanced Usage.

Nesting

Per default, only the properties of the root class are validated.

Sometimes, it is necessary to validate a deeply nested object structure.

To enable validation of the nested object, use the @Nested() decorator.

As with other constraints, @Nested() only triggers if the property is not empty (remember: there's Required() to enforce a non-empty value).

Example:

class NestedClass {
    @Required()
    @MinLength(3)
    value: string;
}

class Root {
    @Nested() // triggers validation of NestedClass if foo is not empty
    foo: NestedClass
}

Validation groups

Sometimes you want to have validations (maybe expensive ones) that should not run on every validation but only when explicitly enabled.

That's where validation groups come into play:

Each constraint is assigned to one ore more validation groups.

If the group is omitted, it's assigned to group 'DEFAULT', a.k.a.: const DEFAULT_GROUP

Groups can be passed on every decorator in the opts parameter:

class GroupTesting {
	@Matches(/Apples/, {groups: ['FIRST']})
	@Matches(/Apples|Oranges/, {groups: ['SECOND']})
	@Matches(/Bananas/, {gropus: ['THIRD']})
	value: string;
	
	constructor(value: string) {
		this.value = value;
	}
}

// just a shorthand to make reading of the examples easier:
const validate = (fixture, groups) => new DecoratorValidator().validate(fixture, groups);


await validate(new GroupTesting("Apples"), {groups: ['FIRST']})
// => success (only FIRST executed, passes)
await validate(new GroupTesting("Apples"), {groups: ['SECOND']})
// => success (only SECOND executed, passes)

await validate(new GroupTesting("Oranges"), {groups: ['FIRST']})
// => fail (only FIRST executed, fails)
await validate(new GroupTesting("Oranges"), {groups: ['SECOND']})
// => success (only SECOND executed, passes)

await validate(new GroupTesting("Apples"), {groups: ['FIRST', 'SECOND', 'THIRD']})
// => failure (FIRST and SECOND executed and pass, THIRD executed and fails)

// example of one group stopping validation of followup groups
await validate(new GroupTesting("Oranges"), {groups: ['FIRST', 'SECOND', 'THIRD']})
// => failure (FIRST executed and failed, all others skipped due to failure in FIRST)

// ordering is important!
await validate(new GroupTesting("Oranges"), {groups: ['THIRD', 'SECOND', 'FIRST']})
// => failure (THIRD executed and failed, all others are skipped due to failure in THIRD)

// no groups!
await validate(new GroupTesting("Foobar"), {groups: []})
// => success (nothing executed)

Group execution loop explained:

  1. Execute all validations in one group.

  2. Break if a validation failure occurred anywhere in that group.

  3. Otherwise continue with the next group at step 1.

Pseudo-code:

  • For every group
    • for each property
      • for each constraint in the current group
        • execute validation
        • break constraint-loop on failure
    • Any error for that group: stop execution and return errors

Build your own validator

You will hit a point when the existing constraints/decorators will not suffice.

at-valid provides two ways to implement your own validations:

  • CustomConstraint(): great for one-off or prototyping purposes.
  • Define your own decorators for repeated use.

Quick and dirty: CustomConstraint()

CustomConstraint() is a predefined decorator where you just have to fill in the validation function and the message, great for one-off or prototyping purposes.

Don't forget: in most cases you should follow our best practices and not fail on empty values.

Example:

import {isEmpty} from 'at-valid/util/isEmpty';

class ClassUsingCustomConstraint {
    @CustomConstraint(
        'MyValidator',
        (theValue, ctx, theInstance) => isEmpty(theValue) || theValue === 'Wanted: Dead or Alive',
        {hello: 'world'}
    )
    value?: string;
}

const fixture = new ClassUsingCustomConstraint();
fixture.value = 'hiding in the darkness';

const errors = await new DecoratorValidator().validate(fixture);

if (errors) {
    console.log(errors);
}

/* prints:
{
    success: false,
    propertyErrors: {
        value: {
            propertyKey: 'value',
            value: 'hiding in the darkness',
            path: '$.value',
            validatorName: 'MyValidator',
            validatorFnContext: {
                args: {hello: 'world'},
                customContext: {}
            }
        }
    }
}
*/

Reusable: CustomConstraint()

Instead of using CustomConstraint() directly in your object, you could wrap it in your own decorator:

export function MyConstraint(someParam: string, opt?: Opts) {
    return CustomConstraint('MyConstraint', () => validate(someParam), {arg1: 'yeah'});
}

// then use it as usual: 
class TestClass {
    @MyConstraint('Thanks for all the fish!')
    value?: string;
}

Reusable: write your own decorator

All shipped constraints are implemented using this method. So if you're in doubt, just have a quick glance at the source of our decorators that is closest to your requirements.

Don't forget: in most cases you should follow our best practices and not fail on empty values.

Konfuzius once said: one line of code tells more than 1000 pictures. So let's have a look at one for reference:

import {isEmpty} from 'at-valid/util/isEmpty';

function FavoriteMovieCharacter(movie: string, opts?: Opts) {
    const movies: { [movie: string]: string } = {
        'Blade Runner': 'Deckard',
        'Dune': 'Chani'
    };

    function isValid(value: any): boolean {
        return isEmpty(value) || movies[movie] === value;
    }

    return (target: object, propertyKey: string) => {
        ValidationContext.instance.registerPropertyValidator({
            // make sure the name does not clash with other validators (see below)
            name: 'FavoriteMovieCharacter',
            messageArgs: {movie},
            target,
            propertyKey,
            validatorFn: value => isValid(value),
            opts
        });
    };
}

// and then use as you would use any other constraint:
class FavoriteMovieCharacter {
    @IsIn('Blade Runner')
    movieCast?: string;
}

For an up-to-date list of predefined validators, see ValidatorNames.ts.

CustomContext

This user-settable parameter available on all decorators is passed to the validation function and eventually to the resulting error object.

For predefined decorators, this enables some way of passing additional arguments to your error message builders.

For your own decorators, this is kinda redundant to decorator parameters.

Example:

class CustomContextTester {
    @Required({customContext: {additionalInfo: 'we reeeeeeeally need this value!'}})
    value?: string;

    constructor(value?: string) {
        this.value = value;
    }
}

const fixture = new CustomContextTester();

const error = await new DecoratorValidator().validate(fixture);

if (error.isError && error.propertyErrors.value) {
  console.log(error.propertyErrors.value.validatorFnContext.customContext.additionalInfo);
}

/* prints:
we reeeeeeeally need this value!
*/

Customize messageArgs in the result

Developing

Here's a brief intro about what a developer must do in order to start developing the project further:

git clone https://github.com/your/awesome-project.git
cd awesome-project/
packagemanager install

And state what happens step-by-step.

Building

If your project needs some additional steps for the developer to build the project after some code changes, state them here:

./configure
make
make install

Here again you should state what actually happens when the code above gets executed.

Deploying / Publishing

In case there's some step you have to take that publishes this project to a server, this is the right time to state it.

packagemanager deploy awesome-project -s server.com -u username -p password

And again you'd need to tell what the previous code actually does.

Features

What's all the bells and whistles this project can perform?

  • What's the main functionality
  • You can also do another thing
  • If you get really randy, you can even do this

Configuration

Here you should write what are all of the configurations a user can enter when using the project.

Argument 1

Type: String
Default: 'default value'

State what an argument does and how you can use it. If needed, you can provide an example below.

Example:

awesome-project "Some other value"  # Prints "You're nailing this readme!"

Argument 2

Type: Number|Boolean
Default: 100

Copy-paste as many of these as you need.

Contributing

When you publish something open source, one of the greatest motivations is that anyone can just jump in and start contributing to your project.

These paragraphs are meant to welcome those kind souls to feel that they are needed. You should state something like:

"If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome."

If there's anything else the developer needs to know (e.g. the code style guide), you should link it here. If there's a lot of things to take into consideration, it is common to separate this section to its own file called CONTRIBUTING.md (or similar). If so, you should say that it exists here.

Links

Even though this information can be found inside the project on machine-readable format like in a .json file, it's good to include a summary of most useful links to humans using your project. You can include links like:

Licensing

The code in this project is licensed under MIT license, see LICENSE.md.