diff --git a/docs/site/Validation-ORM-layer.md b/docs/site/Validation-ORM-layer.md new file mode 100644 index 000000000000..1ec715e0f826 --- /dev/null +++ b/docs/site/Validation-ORM-layer.md @@ -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. diff --git a/docs/site/Validation-REST-layer.md b/docs/site/Validation-REST-layer.md new file mode 100644 index 000000000000..993c64052af4 --- /dev/null +++ b/docs/site/Validation-REST-layer.md @@ -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" %} diff --git a/docs/site/Validation-controller-layer.md b/docs/site/Validation-controller-layer.md new file mode 100644 index 000000000000..4c0515dd2787 --- /dev/null +++ b/docs/site/Validation-controller-layer.md @@ -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, +) { + // 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/) diff --git a/docs/site/Validation.md b/docs/site/Validation.md new file mode 100644 index 000000000000..eaccd267fcab --- /dev/null +++ b/docs/site/Validation.md @@ -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 | diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index bef14ad761ef..3d48fa86508d 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -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'