Skip to content

Commit

Permalink
feat(docs): add validation docs
Browse files Browse the repository at this point in the history
  • Loading branch information
dhmlau committed Mar 13, 2020
1 parent ba722f8 commit bbec3d4
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 0 deletions.
14 changes: 14 additions & 0 deletions docs/site/Validation-ORM-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
lang: en
title: 'Validation in ORM Layer'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Validation-ORM-layer.html
---

The validation in the ORM layer is to make sure the data being added or updated
to the database is valid.

There is validation coming from the legacy juggler. The validation is enforced
at the database level, there is not much additional constraints we can set
beyond the database ones.
163 changes: 163 additions & 0 deletions docs/site/Validation-REST-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
lang: en
title: 'Validation in REST Layer'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Validation-REST-layer.html
---

At the REST layer, there are 2 types of validation:

1. [Type validation](#type-validation)
2. [Validation against OpenAPI schema specification](#validation-against-openapi-schema-specification)

## Type Validation

The type validation in the REST layer comes out of the box in LoopBack.

> Validations are applied on the parameters and the request body data. They also
> use OpenAPI specification as the reference to infer the validation rules.
Take the `capacity` property in the `CoffeeShop` model as an example, it is a
number. When creating a `CoffeeShop` by calling /POST, if a string is specified
for the `capacity` property as below:

```json
{
"city": "Toronto",
"phoneNum": "416-111-1111",
"capacity": "100"
}
```

a "request body is invalid" error is expected:

```json
{
"error": {
"statusCode": 422,
"name": "UnprocessableEntityError",
"message": "The request body is invalid. See error object `details` property for more info.",
"code": "VALIDATION_FAILED",
"details": [
{
"path": ".capacity",
"code": "type",
"message": "should be number",
"info": {
"type": "number"
}
}
]
}
}
```

## Validation against OpenAPI Schema Specification

For validation against an OpenAPI schema specification, the
[AJV module](https://github.com/epoberezkin/ajv) can be used to validate data
with a JSON schema generated from the OpenAPI schema specification.

More details can be found about
[validation keywords](https://github.com/epoberezkin/ajv#validation-keywords)
and
[annotation keywords](https://github.com/epoberezkin/ajv#annotation-keywords)
available in AJV.

Below are a few examples on the usage.

### Example#1: Length limit

A typical validation example is to have a length limit on a string using the
keywords `maxLength` and `minLength`. For example:

```ts
@property({
type: 'string',
required: true,
// --- add jsonSchema -----
jsonSchema: {
maxLength: 10,
minLength: 1,
},
// ------------------------
})
city: string;
```

If the `city` property in the request body does not satisfy the requirement as
follows:

```json
{
"city": "a long city name 123123123",
"phoneNum": "416-111-1111",
"capacity": 10
}
```

an error will occur with details on what has been violated:

```json
{
"error": {
"statusCode": 422,
"name": "UnprocessableEntityError",
"message": "The request body is invalid. See error object `details` property for more info.",
"code": "VALIDATION_FAILED",
"details": [
{
"path": ".city",
"code": "maxLength",
"message": "should NOT be longer than 10 characters",
"info": {
"limit": 10
}
}
]
}
}
```

### Example#2: Value range for a number

For numbers, the validation rules can be used to specify the range of the value.
For example, any coffee shop would not be able to have more than 100 people, it
can be specified as follows:

```ts
@property({
type: 'number',
required: true,
// --- add jsonSchema -----
jsonSchema: {
maximum: 100,
minimum: 1,
},
// ------------------------
})
capacity: number;
```

### Example#3: Pattern in a string

Model properties, such as phone number and postal/zip code, usually have certain
patterns. In this case, the `pattern` keyword can be used to specify the
restrictions.

Below shows an example of the expected pattern of phone numbers, i.e. a sequence
of 10 digits separated by `-` after the 3rd and 6th digits.

```ts
@property({
type: 'string',
required: true,
// --- add jsonSchema -----
jsonSchema: {
pattern: '\\d{3}-\\d{3}-\\d{4}',
},
// ------------------------
})
phoneNum: string;
```
128 changes: 128 additions & 0 deletions docs/site/Validation-controller-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
lang: en
title: 'Validation in the Controller Layer'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Validation-controller-layer.html
---

In the case where validation rules are not static, validations cannot be
specified at the model level. Hence, validation can be added in the controller
layer.

Take an example of a promo code in an order, it is usually a defined value that
only valid for a certain period of time. And in the CoffeeShop example, the area
code of a phone number usually depends on the geolocation.

## Add validation function in the Controller method

The simplest way is to apply the validation function in the controller method,
that is:

```ts
// create a validatePhoneNum function and call it here
if (!this.validatePhoneNum(coffeeShop.phoneNum, coffeeShop.city))
throw new Error('Area code in phone number and city do not match.');
return this.coffeeShopRepository.create(coffeeShop);
```

## Add interceptor for validation

Another way is to use [interceptors](Interceptors.md).

> Interceptors are reusable functions to provide aspect-oriented logic around
> method invocations.
Interceptors have access to the invocation context, including parameter values
for the method call. It can perform more specific validations, for example,
calling a service to check if an address is valid. There are three types of
interceptors for different scopes: global, class-level and method-level
interceptors.

Interceptors can be created using the
[interceptor generator](https://loopback.io/doc/en/lb4/Interceptor-generator.html)
`lb4 interceptor` command. In the CoffeeShop example, the `phoneNum` in the
`CoffeeShop` request body will be validated for the `POST` and `PUT` call
whether the area code in the phone number matches the specified city. Since this
is only applicable to the CoffeeShop endpoints, a non-global interceptor is
created, i.e. specify `No` in the `Is it a global interceptor` prompt.

```sh
$ lb4 interceptor
? Interceptor name: validatePhoneNum
? Is it a global interceptor? No
create src/interceptors/validate-phone-num.interceptor.ts
update src/interceptors/index.ts

Interceptor ValidatePhoneNum was created in src/interceptors/
```

In the newly created interceptor `ValidatePhoneNumInterceptor`, the function
`intercept` is the place where the pre-invocation and post-invocation logic can
be added.

{% include code-caption.html content="/src/interceptors/validate-phone-num-interceptor.ts" %}

```ts
async intercept(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<InvocationResult>,
) {
try {
// Add pre-invocation logic here
// ------ VALIDATE PHONE NUMBER ----------
const coffeeShop: CoffeeShop = new CoffeeShop();
if (invocationCtx.methodName == 'create')
Object.assign(coffeeShop, invocationCtx.args[0]);
else if (invocationCtx.methodName == 'updateById')
Object.assign(coffeeShop, invocationCtx.args[1]);

if (
coffeeShop &&
!this.isAreaCodeValid(coffeeShop.phoneNum, coffeeShop.city)
) {
throw new HttpErrors.InternalServerError(
'Area code and city do not match',
);
}
// ----------------------------------------

const result = await next();
// Add post-invocation logic here
return result;
} catch (err) {
// Add error handling logic here
throw err;
}
}

isAreaCodeValid(phoneNum: string, city: string): Boolean {
// add some logic here
// it always returns true for now
return true;
}
```

Now that the interceptor is created, we are going to apply this to the
controller with the `CoffeeShop` endpoints, `CoffeeShopController`.

{% include code-caption.html content="/src/controllers/coffee-shop.controller.ts" %}

```ts
// Add these imports for interceptors
import {inject, intercept} from '@loopback/core';
import {ValidatePhoneNumInterceptor} from '../interceptors';

// Add this line to apply interceptor to this class
@intercept(ValidatePhoneNumInterceptor.BINDING_KEY)
export class CoffeeShopController {
// ....
}
```

## Reference

To find out more about interceptors, check out the blog posts below:

- [Learning LoopBack 4 Interceptors (Part 1) - Global Interceptors](https://strongloop.com/strongblog/loopback4-interceptors-part1/)
- [Learning LoopBack 4 Interceptors (Part 2) - Method Level and Class Level Interceptors](https://strongloop.com/strongblog/loopback4-interceptors-part2/)
28 changes: 28 additions & 0 deletions docs/site/Validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
lang: en
title: 'Validation'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Validation.html
---

Within a LoopBack application, validation can be added in various places
depending on the usage. Some types of validations come out-of-the-box in
LoopBack, such as type validation in the REST layer, whereas some require
additional configuration or code.

For illustration purpose, a `CoffeeShop` model is being used. It has the
following properties.

| Property name | Type | Description |
| ------------- | :----: | ------------------------------------- |
| shopId | string | ID of the coffee shop |
| city | string | City where the coffee shop is located |
| phoneNum | string | Phone number of the coffee shop |
| capacity | number | Capacity of the coffee shop |

Let's take a closer look at how validation can be added in the following layers:

- [REST layer](Validation-REST-layer.md)
- [Controller layer](Validation-controller-layer.md)
- [ORM layer](Validation-ORM-layer.md)
16 changes: 16 additions & 0 deletions docs/site/sidebars/lb4_sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,22 @@ children:
url: Serving-static-files.html
output: 'web, pdf'

- title: 'Validation'
url: Validation.html
output: 'web, pdf'
children:
- title: 'Validation in REST Layer'
url: Validation-REST-layer.html
output: 'web, pdf'

- title: 'Validation in the Controller Layer'
url: Validation-controller-layer.html
output: 'web, pdf'

- title: 'Validation in ORM Layer'
url: Validation-ORM-layer.html
output: 'web, pdf'

- title: 'Behind the Scene'
url: Behind-the-scene.html
output: 'web, pdf'
Expand Down

0 comments on commit bbec3d4

Please sign in to comment.