Skip to content

Commit

Permalink
feat: add validation page
Browse files Browse the repository at this point in the history
Closes: #27
  • Loading branch information
Romakita committed Apr 15, 2024
1 parent 458300a commit e5b2f20
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .gflowrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"flow": "gflow",
"remote": "origin",
"develop": "main",
"production": "main",
"ignores": [],
"syncAfterFinish": false,
"postFinish": "",
"skipTest": false,
"charReplacement": "-",
"charBranchNameSeparator": "-",
"branchTypes": {
"feat": "feat",
"fix": "fix",
"chore": "chore",
"docs": "docs"
},
"refs": {}
}
11 changes: 11 additions & 0 deletions docs/docs/snippets/validation/class-transformer-pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {DeserializerPipe} from "@tsed/platform-params";
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
import {OverrideProvider} from "@tsed/di";
import {plainToClass} from "class-transformer";

@OverrideProvider(DeserializerPipe)
export class ClassTransformerPipe implements PipeMethods {
transform(value: any, metadata: JsonParameterStore) {
return plainToClass(metadata.type, value);
}
}
30 changes: 30 additions & 0 deletions docs/docs/snippets/validation/class-validator-pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {ValidationError, ValidationPipe} from "@tsed/platform-params";
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
import {OverrideProvider} from "@tsed/di";
import {plainToClass} from "class-transformer";
import {validate} from "class-validator";

@OverrideProvider(ValidationPipe)
export class ClassValidationPipe extends ValidationPipe implements PipeMethods<any> {
async transform(value: any, metadata: JsonParameterStore) {
if (!this.shouldValidate(metadata)) {
// there is no type and collectionType
return value;
}

const object = plainToClass(metadata.type, value);
const errors = await validate(object);

if (errors.length > 0) {
throw new ValidationError("Oops something is wrong", errors);
}

return value;
}

protected shouldValidate(metadata: JsonParameterStore): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];

return !(metadata.type || metadata.collectionType) || !types.includes(metadata.type);
}
}
7 changes: 7 additions & 0 deletions docs/docs/snippets/validation/joi-pipe-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {ObjectSchema} from "joi";
import {StoreSet} from "@tsed/core";
import {JoiValidationPipe} from "../pipes/JoiValidationPipe";

export function UseJoiSchema(schema: ObjectSchema) {
return StoreSet(JoiValidationPipe, schema);
}
17 changes: 17 additions & 0 deletions docs/docs/snippets/validation/joi-pipe-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {BodyParams} from "@tsed/platform-params";
import {Get} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {UseJoiSchema} from "../decorators/UseJoiSchema";
import {joiPersonModel, PersonModel} from "../models/PersonModel";

@Controller("/persons")
export class PersonsController {
@Get(":id")
async findOne(
@BodyParams("id")
@UseJoiSchema(joiPersonModel)
person: PersonModel
) {
return person;
}
}
21 changes: 21 additions & 0 deletions docs/docs/snippets/validation/joi-pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {ObjectSchema} from "joi";
import {Injectable} from "@tsed/di";
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
import {ValidationError, ValidationPipe} from "@tsed/platform-params";

@OverrideProvider(ValidationPipe)
export class JoiValidationPipe implements PipeMethods {
transform(value: any, metadata: JsonParameterStore) {
const schema = metadata.store.get<ObjectSchema>(JoiValidationPipe);

if (schema) {
const {error} = schema.validate(value);

if (error) {
throw new ValidationError("Oops something is wrong", [error]);
}
}

return value;
}
}
23 changes: 23 additions & 0 deletions docs/docs/snippets/validation/validator-pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {ValidationError, ValidationPipe} from "@tsed/platform-params";
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
import {OverrideProvider} from "@tsed/di";
import {getJsonSchema} from "@tsed/schema";
import {validate} from "./validate";

@OverrideProvider(ValidationPipe)
export class CustomValidationPipe extends ValidationPipe implements PipeMethods {
public transform(obj: any, metadata: JsonParameterStore): void {
// JSON service contain tool to build the Schema definition of a model.
const schema = getJsonSchema(metadata.type);

if (schema) {
const valid = validate(schema, obj);

if (!valid) {
throw new ValidationError("My message", [
/// list of errors
]);
}
}
}
}
168 changes: 168 additions & 0 deletions docs/docs/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Validation

Ts.ED provide by default a [AJV](/tutorials/ajv.md) package `@tsed/ajv` to perform a validation on a [Model](/docs/model.md).

This package must be installed to run automatic validation on input data. Any model used on parameter and annotated with one of JsonSchema decorator will be
validated with AJV.

::: code-group

```sh [npm]
npm install --save @tsed/ajv
```

```sh [yarn]
yarn add @tsed/ajv
```

```sh [pnpm]
pnpm add @tsed/ajv
```

```sh [bun]
bun add @tsed/ajv
```
:::

## Data input validation

Ts.ED support the data input validation with the decorators provided by `@tsed/schema`.

Example:

<<< @/docs/snippets/controllers/request-input-validation.ts

## Custom Validation

Ts.ED allows you to change the default @@ValidationPipe@@ by your own library. The principle is simple.
Create a CustomValidationPipe and use @@OverrideProvider@@ to change the default @@ValidationPipe@@.

::: warning
Replace the default JsonSchema validation provided by Ts.ED isn't recommended. You lose the ability to generate the swagger documentation and the json-mapper feature.
:::

<<< @/docs/snippets/validation/validator-pipe.ts

::: warning
Don't forgot to import the new `CustomValidatorPipe` in your `server.ts` !
:::

### Use Joi

There are several approaches available for object validation. One common approach is to use schema-based validation.
The [Joi](https://github.com/hapijs/joi) library allows you to create schemas in a pretty straightforward way, with a readable API.

Let's look at a pipe that makes use of Joi-based schemas.

Start by installing the required package:

::: code-group

```sh [npm]
npm install --save joi
```

```sh [yarn]
yarn add joi
```

```sh [pnpm]
pnpm add joi
```

```sh [bun]
bun add joi
```

:::

In the code sample below, we create a simple class that takes a schema as a constructor argument.
We then apply the `schema.validate()` method, which validates our incoming argument against the provided schema.

In the next section, you'll see how we supply the appropriate schema for a given controller method using the @@UsePipe@@ decorator.

<<< @/docs/snippets/validation/joi-pipe.ts

Now, we have to create a custom decorator to store the Joi schema along with a parameter:

<<< @/docs/snippets/validation/joi-pipe-decorator.ts

And finally, we are able to add Joi schema with our new decorator:

<<< @/docs/snippets/validation/joi-pipe-usage.ts

### Use Class validator

Let's look at an alternate implementation of our validation technique.

Ts.ED works also with the [class-validator](https://github.com/typestack/class-validator) library.
This library allows you to use **decorator-based** validation (like Ts.ED with his [JsonSchema](/docs/models) decorators).
Decorator-based validation combined with Ts.ED [Pipe](/docs/pipes.html) capabilities since we have access to the medata.type of the processed parameter.

Before we start, we need to install the required packages:

::: code-group

```sh [npm]
npm i --save class-validator class-transformer
```

```sh [yarn]
yarn add class-validator class-transformer
```

```sh [pnpm]
pnpm add class-validator class-transformer
```

```sh [bun]
bun add class-validator class-transformer
```

:::

Once these are installed, we can add a few decorators to the `PersonModel`:

```typescript
import {IsString, IsInt} from "class-validator";

export class CreateCatDto {
@IsString()
firstName: string;

@IsInt()
age: number;
}
```

::: tip
Read more about the class-validator decorators [here](https://github.com/typestack/class-validator#usage).
:::

Now we can create a [ClassValidationPipe] class:

<<< @/docs/snippets/validation/class-validator-pipe.ts

::: warning Notice
Above, we have used the [class-transformer](https://github.com/typestack/class-transformer) library.
It's made by the same author as the **class-validator** library, and as a result, they play very well together.
:::

Note that we get the type from @@ParamMetadata@@ and give it to plainToObject function. The method `shouldValidate`
bypass the validation process for the basic types and when the `metadata.type` or `metadata.collectionType` are not available.

Next, we use the **class-transformer** function `plainToClass()` to transform our plain JavaScript argument object into a typed object
so that we can apply validation. The incoming body, when deserialized from the network request, does not have any type information.
Class-validator needs to use the validation decorators we defined for our **PersonModel** earlier,
so we need to perform this transformation.

Finally, we return the value when we haven't errors or throws a `ValidationError`.

::: tip
If you use **class-validator**, it also be logical to use [class-transformer](https://github.com/typestack/class-transformer) as Deserializer.
So we recommend to override also the @@DeserializerPipe@@.

<<< @/docs/snippets/validation/class-transformer-pipe.ts
:::

We just have to import the pipe on our `server.ts` and use model as type on a parameter.

0 comments on commit e5b2f20

Please sign in to comment.