Skip to content

Commit 132ef14

Browse files
committed
feat: add validation page
Closes: #27
1 parent c3c0b5b commit 132ef14

File tree

8 files changed

+296
-0
lines changed

8 files changed

+296
-0
lines changed

.gflowrc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"flow": "gflow",
3+
"remote": "origin",
4+
"develop": "main",
5+
"production": "main",
6+
"ignores": [],
7+
"syncAfterFinish": false,
8+
"postFinish": "",
9+
"skipTest": false,
10+
"charReplacement": "-",
11+
"charBranchNameSeparator": "-",
12+
"branchTypes": {
13+
"feat": "feat",
14+
"fix": "fix",
15+
"chore": "chore",
16+
"docs": "docs"
17+
},
18+
"refs": {}
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {DeserializerPipe} from "@tsed/platform-params";
2+
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
3+
import {OverrideProvider} from "@tsed/di";
4+
import {plainToClass} from "class-transformer";
5+
6+
@OverrideProvider(DeserializerPipe)
7+
export class ClassTransformerPipe implements PipeMethods {
8+
transform(value: any, metadata: JsonParameterStore) {
9+
return plainToClass(metadata.type, value);
10+
}
11+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {ValidationError, ValidationPipe} from "@tsed/platform-params";
2+
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
3+
import {OverrideProvider} from "@tsed/di";
4+
import {plainToClass} from "class-transformer";
5+
import {validate} from "class-validator";
6+
7+
@OverrideProvider(ValidationPipe)
8+
export class ClassValidationPipe extends ValidationPipe implements PipeMethods<any> {
9+
async transform(value: any, metadata: JsonParameterStore) {
10+
if (!this.shouldValidate(metadata)) {
11+
// there is no type and collectionType
12+
return value;
13+
}
14+
15+
const object = plainToClass(metadata.type, value);
16+
const errors = await validate(object);
17+
18+
if (errors.length > 0) {
19+
throw new ValidationError("Oops something is wrong", errors);
20+
}
21+
22+
return value;
23+
}
24+
25+
protected shouldValidate(metadata: JsonParameterStore): boolean {
26+
const types: Function[] = [String, Boolean, Number, Array, Object];
27+
28+
return !(metadata.type || metadata.collectionType) || !types.includes(metadata.type);
29+
}
30+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {ObjectSchema} from "joi";
2+
import {StoreSet} from "@tsed/core";
3+
import {JoiValidationPipe} from "../pipes/JoiValidationPipe";
4+
5+
export function UseJoiSchema(schema: ObjectSchema) {
6+
return StoreSet(JoiValidationPipe, schema);
7+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {BodyParams} from "@tsed/platform-params";
2+
import {Get} from "@tsed/schema";
3+
import {Controller} from "@tsed/di";
4+
import {UseJoiSchema} from "../decorators/UseJoiSchema";
5+
import {joiPersonModel, PersonModel} from "../models/PersonModel";
6+
7+
@Controller("/persons")
8+
export class PersonsController {
9+
@Get(":id")
10+
async findOne(
11+
@BodyParams("id")
12+
@UseJoiSchema(joiPersonModel)
13+
person: PersonModel
14+
) {
15+
return person;
16+
}
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {ObjectSchema} from "joi";
2+
import {Injectable} from "@tsed/di";
3+
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
4+
import {ValidationError, ValidationPipe} from "@tsed/platform-params";
5+
6+
@OverrideProvider(ValidationPipe)
7+
export class JoiValidationPipe implements PipeMethods {
8+
transform(value: any, metadata: JsonParameterStore) {
9+
const schema = metadata.store.get<ObjectSchema>(JoiValidationPipe);
10+
11+
if (schema) {
12+
const {error} = schema.validate(value);
13+
14+
if (error) {
15+
throw new ValidationError("Oops something is wrong", [error]);
16+
}
17+
}
18+
19+
return value;
20+
}
21+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {ValidationError, ValidationPipe} from "@tsed/platform-params";
2+
import {JsonParameterStore, PipeMethods} from "@tsed/schema";
3+
import {OverrideProvider} from "@tsed/di";
4+
import {getJsonSchema} from "@tsed/schema";
5+
import {validate} from "./validate";
6+
7+
@OverrideProvider(ValidationPipe)
8+
export class CustomValidationPipe extends ValidationPipe implements PipeMethods {
9+
public transform(obj: any, metadata: JsonParameterStore): void {
10+
// JSON service contain tool to build the Schema definition of a model.
11+
const schema = getJsonSchema(metadata.type);
12+
13+
if (schema) {
14+
const valid = validate(schema, obj);
15+
16+
if (!valid) {
17+
throw new ValidationError("My message", [
18+
/// list of errors
19+
]);
20+
}
21+
}
22+
}
23+
}

docs/docs/validation.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Validation
2+
3+
Ts.ED provide by default a [AJV](/tutorials/ajv.md) package `@tsed/ajv` to perform a validation on a [Model](/docs/model).
4+
5+
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
6+
validated with AJV.
7+
8+
::: code-group
9+
10+
```sh [npm]
11+
npm install --save @tsed/ajv
12+
```
13+
14+
```sh [yarn]
15+
yarn add @tsed/ajv
16+
```
17+
18+
```sh [pnpm]
19+
pnpm add @tsed/ajv
20+
```
21+
22+
```sh [bun]
23+
bun add @tsed/ajv
24+
```
25+
:::
26+
27+
## Data input validation
28+
29+
Ts.ED support the data input validation with the decorators provided by `@tsed/schema`.
30+
31+
Example:
32+
33+
<<< @/docs/snippets/controllers/request-input-validation.ts
34+
35+
## Custom Validation
36+
37+
Ts.ED allows you to change the default @@ValidationPipe@@ by your own library. The principle is simple.
38+
Create a CustomValidationPipe and use @@OverrideProvider@@ to change the default @@ValidationPipe@@.
39+
40+
::: warning
41+
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.
42+
:::
43+
44+
<<< @/docs/snippets/validation/validator-pipe.ts
45+
46+
::: warning
47+
Don't forgot to import the new `CustomValidatorPipe` in your `server.ts` !
48+
:::
49+
50+
### Use Joi
51+
52+
There are several approaches available for object validation. One common approach is to use schema-based validation.
53+
The [Joi](https://github.com/hapijs/joi) library allows you to create schemas in a pretty straightforward way, with a readable API.
54+
55+
Let's look at a pipe that makes use of Joi-based schemas.
56+
57+
Start by installing the required package:
58+
59+
::: code-group
60+
61+
```sh [npm]
62+
npm install --save joi
63+
```
64+
65+
```sh [yarn]
66+
yarn add joi
67+
```
68+
69+
```sh [pnpm]
70+
pnpm add joi
71+
```
72+
73+
```sh [bun]
74+
bun add joi
75+
```
76+
77+
:::
78+
79+
In the code sample below, we create a simple class that takes a schema as a constructor argument.
80+
We then apply the `schema.validate()` method, which validates our incoming argument against the provided schema.
81+
82+
In the next section, you'll see how we supply the appropriate schema for a given controller method using the @@UsePipe@@ decorator.
83+
84+
<<< @/docs/snippets/validation/joi-pipe.ts
85+
86+
Now, we have to create a custom decorator to store the Joi schema along with a parameter:
87+
88+
<<< @/docs/snippets/validation/joi-pipe-decorator.ts
89+
90+
And finally, we are able to add Joi schema with our new decorator:
91+
92+
<<< @/docs/snippets/validation/joi-pipe-usage.ts
93+
94+
### Use Class validator
95+
96+
Let's look at an alternate implementation of our validation technique.
97+
98+
Ts.ED works also with the [class-validator](https://github.com/typestack/class-validator) library.
99+
This library allows you to use **decorator-based** validation (like Ts.ED with his [JsonSchema](/docs/model) decorators).
100+
Decorator-based validation combined with Ts.ED [Pipe](/docs/pipes.html) capabilities since we have access to the medata.type of the processed parameter.
101+
102+
Before we start, we need to install the required packages:
103+
104+
::: code-group
105+
106+
```sh [npm]
107+
npm i --save class-validator class-transformer
108+
```
109+
110+
```sh [yarn]
111+
yarn add class-validator class-transformer
112+
```
113+
114+
```sh [pnpm]
115+
pnpm add class-validator class-transformer
116+
```
117+
118+
```sh [bun]
119+
bun add class-validator class-transformer
120+
```
121+
122+
:::
123+
124+
Once these are installed, we can add a few decorators to the `PersonModel`:
125+
126+
```typescript
127+
import {IsString, IsInt} from "class-validator";
128+
129+
export class CreateCatDto {
130+
@IsString()
131+
firstName: string;
132+
133+
@IsInt()
134+
age: number;
135+
}
136+
```
137+
138+
::: tip
139+
Read more about the class-validator decorators [here](https://github.com/typestack/class-validator#usage).
140+
:::
141+
142+
Now we can create a [ClassValidationPipe] class:
143+
144+
<<< @/docs/snippets/validation/class-validator-pipe.ts
145+
146+
::: warning Notice
147+
Above, we have used the [class-transformer](https://github.com/typestack/class-transformer) library.
148+
It's made by the same author as the **class-validator** library, and as a result, they play very well together.
149+
:::
150+
151+
Note that we get the type from @@ParamMetadata@@ and give it to plainToObject function. The method `shouldValidate`
152+
bypass the validation process for the basic types and when the `metadata.type` or `metadata.collectionType` are not available.
153+
154+
Next, we use the **class-transformer** function `plainToClass()` to transform our plain JavaScript argument object into a typed object
155+
so that we can apply validation. The incoming body, when deserialized from the network request, does not have any type information.
156+
Class-validator needs to use the validation decorators we defined for our **PersonModel** earlier,
157+
so we need to perform this transformation.
158+
159+
Finally, we return the value when we haven't errors or throws a `ValidationError`.
160+
161+
::: tip
162+
If you use **class-validator**, it also be logical to use [class-transformer](https://github.com/typestack/class-transformer) as Deserializer.
163+
So we recommend to override also the @@DeserializerPipe@@.
164+
165+
<<< @/docs/snippets/validation/class-transformer-pipe.ts
166+
:::
167+
168+
We just have to import the pipe on our `server.ts` and use model as type on a parameter.

0 commit comments

Comments
 (0)