UNPKG

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