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 27, 2020
1 parent 85c12eb commit 3506e5d
Show file tree
Hide file tree
Showing 5 changed files with 383 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.

Schema constraints are enforced by specific databases, such as unique index. For
pre-processing against CRUD operations, see details:
https://loopback.io/doc/en/lb4/migration-models-operation-hooks.html.
177 changes: 177 additions & 0 deletions docs/site/Validation-REST-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
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, request body is being validated against the OpenAPI schema
specification.

## Type Validation

Type validation comes out-of-the-box in LoopBack.

> Validation is applied on the parameters and the request body data. It also
> uses OpenAPI specification as the reference to infer the validation rules.
Take the `capacity` property in the [`CoffeeShop` model](Validation.md) 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) is 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. AJV can also be extended with custom keywords and formats, see
[AJV defining custom keywords page](https://ajv.js.org/custom.html).

Besides AJV, other third-party validation libraries, such as
[@hapi/joi](https://github.com/hapijs/joi) and
[class-validator](https://github.com/typestack/class-validator), can be used.

Below are a few examples of using AJV for validation. The source code of the
snippets can be found in the
[coffee-shop.model.ts in the example app](https://github.com/strongloop/loopback-next/blob/master/examples/validation-app/src/models/coffee-shop.model.ts).

{% include note.html content="The `jsonSchema` property expects [JSON Schema Draft-07](http://json-schema.org/draft/2019-09/json-schema-validation.html), which is then transformed into the [OAS 3](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md) variant." %}

### 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:

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

```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 are 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:

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

```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 is 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.

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

```ts
@property({
type: 'string',
required: true,
// --- add jsonSchema -----
jsonSchema: {
pattern: '\\d{3}-\\d{3}-\\d{4}',
},
// ------------------------
})
phoneNum: string;
```

{% include tip.html content="RegExp can be converted into a string with `.source` to avoid escaping backslashes" %}
140 changes: 140 additions & 0 deletions docs/site/Validation-controller-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
lang: en
title: 'Validation in the Controller, Repository and Service Layer'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Validation-controller-repo-service-layer.html
---

In the case where validation rules are not static, validation 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
is 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.
For example:

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

```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 validation, 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` calls
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>,
) {
// Add pre-invocation logic here
// ------ VALIDATE PHONE NUMBER ----------
let coffeeShop: CoffeeShop | undefined;
if (invocationCtx.methodName === 'create')
coffeeShop = invocationCtx.args[0];
else if (invocationCtx.methodName === 'updateById')
coffeeShop = invocationCtx.args[1];

if (
coffeeShop &&
!this.isAreaCodeValid(coffeeShop.phoneNum, coffeeShop.city)
) {
const err: ValidationError = new ValidationError(
'Area code and city do not match',
);
err.statusCode = 422;
throw err;
}
// ----------------------------------------

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 dummy logic
const areaCode: string = phoneNum.slice(0, 3);
if (
!(
city.toLowerCase() === 'toronto' &&
(areaCode === '416' || areaCode === '647')
)
)
return false;

// otherwise it always return true
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/)
36 changes: 36 additions & 0 deletions docs/site/Validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
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.

There are various types of validations such as:

- validation of input/output for method invocations
- validation of model instance properties, for example, age < 0
- validation of model collections, for example, uniqueness

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

- [REST layer](Validation-REST-layer.md)
- [Controller, Repository and Service Layer](Validation-controller-layer.md)
- [ORM layer](Validation-ORM-layer.md)

The
[validation-app example application](https://github.com/strongloop/loopback-next/blob/master/examples/validation-app)
is used in the following documentation pages for demonstration. In the example,
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 |
16 changes: 16 additions & 0 deletions docs/site/sidebars/lb4_sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,22 @@ children:
url: File-upload-download.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, Repository and Service Layer'
url: Validation-controller-repo-service-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 3506e5d

Please sign in to comment.