UNPKG

7.28 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. 2018,2020. All Rights Reserved.
3// Node module: @loopback/rest
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6Object.defineProperty(exports, "__esModule", { value: true });
7exports.validateValueAgainstSchema = exports.validateRequestBody = void 0;
8const tslib_1 = require("tslib");
9const openapi_v3_1 = require("@loopback/openapi-v3");
10const debug_1 = tslib_1.__importDefault(require("debug"));
11const util_1 = tslib_1.__importDefault(require("util"));
12const __1 = require("..");
13const ajv_factory_provider_1 = require("./ajv-factory.provider");
14const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema');
15const debug = (0, debug_1.default)('loopback:rest:validation');
16/**
17 * Check whether the request body is valid according to the provided OpenAPI schema.
18 * The JSON schema is generated from the OpenAPI schema which is typically defined
19 * by `@requestBody()`.
20 * The validation leverages AJV schema validator.
21 * @param body - The request body parsed from an HTTP request.
22 * @param requestBodySpec - The OpenAPI requestBody specification defined in `@requestBody()`.
23 * @param globalSchemas - The referenced schemas generated from `OpenAPISpec.components.schemas`.
24 * @param options - Request body validation options for AJV
25 */
26async function validateRequestBody(body, requestBodySpec, globalSchemas = {}, options = ajv_factory_provider_1.DEFAULT_AJV_VALIDATION_OPTIONS) {
27 const required = requestBodySpec === null || requestBodySpec === void 0 ? void 0 : requestBodySpec.required;
28 if (required && body.value == null) {
29 const err = Object.assign(new __1.HttpErrors.BadRequest('Request body is required'), {
30 code: 'MISSING_REQUIRED_PARAMETER',
31 parameterName: 'request body',
32 });
33 throw err;
34 }
35 if (!required && !body.value)
36 return;
37 const schema = body.schema;
38 /* istanbul ignore if */
39 if (debug.enabled) {
40 debug('Request body schema:', util_1.default.inspect(schema, { depth: null }));
41 if (schema &&
42 (0, openapi_v3_1.isReferenceObject)(schema) &&
43 schema.$ref.startsWith('#/components/schemas/')) {
44 const ref = schema.$ref.slice('#/components/schemas/'.length);
45 debug(' referencing:', util_1.default.inspect(globalSchemas[ref], { depth: null }));
46 }
47 }
48 if (!schema)
49 return;
50 options = { coerceTypes: !!body.coercionRequired, ...options };
51 await validateValueAgainstSchema(body.value, schema, globalSchemas, {
52 ...options,
53 source: 'body',
54 });
55}
56exports.validateRequestBody = validateRequestBody;
57/**
58 * Convert an OpenAPI schema to the corresponding JSON schema.
59 * @param openapiSchema - The OpenAPI schema to convert.
60 */
61function convertToJsonSchema(openapiSchema) {
62 const jsonSchema = toJsonSchema(openapiSchema);
63 delete jsonSchema['$schema'];
64 /* istanbul ignore if */
65 if (debug.enabled) {
66 debug('Converted OpenAPI schema to JSON schema: %s', util_1.default.inspect(jsonSchema, { depth: null }));
67 }
68 return jsonSchema;
69}
70/**
71 * Built-in cache for complied schemas by AJV
72 */
73const DEFAULT_COMPILED_SCHEMA_CACHE = new WeakMap();
74/**
75 * Build a cache key for AJV options
76 * @param options - Request body validation options
77 */
78function getKeyForOptions(options = ajv_factory_provider_1.DEFAULT_AJV_VALIDATION_OPTIONS) {
79 const ajvOptions = {};
80 // Sort keys for options
81 const keys = Object.keys(options).sort();
82 for (const k of keys) {
83 if (k === 'compiledSchemaCache')
84 continue;
85 ajvOptions[k] = options[k];
86 }
87 return JSON.stringify(ajvOptions);
88}
89/**
90 * Validate the value against JSON schema.
91 * @param value - The data value.
92 * @param schema - The JSON schema used to perform the validation.
93 * @param globalSchemas - Schema references.
94 * @param options - Value validation options.
95 */
96async function validateValueAgainstSchema(
97// eslint-disable-next-line @typescript-eslint/no-explicit-any
98value, schema, globalSchemas = {}, options = {}) {
99 var _a, _b, _c;
100 let validate;
101 const cache = (_a = options.compiledSchemaCache) !== null && _a !== void 0 ? _a : DEFAULT_COMPILED_SCHEMA_CACHE;
102 const key = getKeyForOptions(options);
103 let validatorMap;
104 if (cache.has(schema)) {
105 validatorMap = cache.get(schema);
106 validate = validatorMap.get(key);
107 }
108 if (!validate) {
109 const ajvFactory = (_b = options.ajvFactory) !== null && _b !== void 0 ? _b : new ajv_factory_provider_1.AjvFactoryProvider(options).value();
110 const ajvInst = ajvFactory(options);
111 validate = createValidator(schema, globalSchemas, ajvInst);
112 validatorMap = validatorMap !== null && validatorMap !== void 0 ? validatorMap : new Map();
113 validatorMap.set(key, validate);
114 cache.set(schema, validatorMap);
115 }
116 let validationErrors = [];
117 try {
118 const validationResult = validate(value);
119 debug(`Value from ${options.source} passed AJV validation.`, validationResult);
120 return await validationResult;
121 }
122 catch (error) {
123 validationErrors = error.errors;
124 }
125 /* istanbul ignore if */
126 if (debug.enabled) {
127 debug('Invalid value: %s. Errors: %s', util_1.default.inspect(value, { depth: null }), util_1.default.inspect(validationErrors));
128 }
129 if (typeof options.ajvErrorTransformer === 'function') {
130 validationErrors = options.ajvErrorTransformer(validationErrors);
131 }
132 // Throw invalid request body error
133 if (options.source === 'body') {
134 const error = __1.RestHttpErrors.invalidRequestBody(buildErrorDetails(validationErrors));
135 throw error;
136 }
137 // Throw invalid value error
138 const error = __1.RestHttpErrors.invalidData(value, (_c = options.name) !== null && _c !== void 0 ? _c : '(unknown)', {
139 details: buildErrorDetails(validationErrors),
140 });
141 throw error;
142}
143exports.validateValueAgainstSchema = validateValueAgainstSchema;
144function buildErrorDetails(validationErrors) {
145 return validationErrors.map((e) => {
146 var _a;
147 return {
148 path: e.instancePath,
149 code: e.keyword,
150 message: (_a = e.message) !== null && _a !== void 0 ? _a : `must pass validation rule ${e.keyword}`,
151 info: e.params,
152 };
153 });
154}
155/**
156 * Create a validate function for the given schema
157 * @param schema - JSON schema for the target
158 * @param globalSchemas - Global schemas
159 * @param ajvInst - An instance of Ajv
160 */
161function createValidator(schema, globalSchemas = {}, ajvInst) {
162 const jsonSchema = convertToJsonSchema(schema);
163 // Clone global schemas to set `$async: true` flag
164 const schemas = {};
165 for (const name in globalSchemas) {
166 // See https://github.com/loopbackio/loopback-next/issues/4939
167 schemas[name] = { ...globalSchemas[name], $async: true };
168 }
169 const schemaWithRef = { components: { schemas }, ...jsonSchema };
170 // See https://js.org/#asynchronous-validation for async validation
171 schemaWithRef.$async = true;
172 return ajvInst.compile(schemaWithRef);
173}
174//# sourceMappingURL=request-body.validator.js.map
\No newline at end of file