UNPKG

@loopback/rest

Version:

Expose controllers as REST endpoints and route REST API requests to controller methods

171 lines 7.29 kB
"use strict"; // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. // Node module: @loopback/rest // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT Object.defineProperty(exports, "__esModule", { value: true }); exports.validateValueAgainstSchema = exports.validateRequestBody = void 0; const tslib_1 = require("tslib"); const openapi_v3_1 = require("@loopback/openapi-v3"); const node_util_1 = tslib_1.__importDefault(require("node:util")); const debug_1 = tslib_1.__importDefault(require("debug")); const __1 = require(".."); const ajv_factory_provider_1 = require("./ajv-factory.provider"); const { openapiSchemaToJsonSchema: toJsonSchema, } = require('@openapi-contrib/openapi-schema-to-json-schema'); const debug = (0, debug_1.default)('loopback:rest:validation'); /** * Check whether the request body is valid according to the provided OpenAPI schema. * The JSON schema is generated from the OpenAPI schema which is typically defined * by `@requestBody()`. * The validation leverages AJV schema validator. * @param body - The request body parsed from an HTTP request. * @param requestBodySpec - The OpenAPI requestBody specification defined in `@requestBody()`. * @param globalSchemas - The referenced schemas generated from `OpenAPISpec.components.schemas`. * @param options - Request body validation options for AJV */ async function validateRequestBody(body, requestBodySpec, globalSchemas = {}, options = ajv_factory_provider_1.DEFAULT_AJV_VALIDATION_OPTIONS) { const required = requestBodySpec === null || requestBodySpec === void 0 ? void 0 : requestBodySpec.required; if (required && body.value == null) { throw Object.assign(new __1.HttpErrors.BadRequest('Request body is required'), { code: 'MISSING_REQUIRED_PARAMETER', parameterName: 'request body', }); } if (!required && !body.value) return; const schema = body.schema; /* istanbul ignore if */ if (debug.enabled) { debug('Request body schema:', node_util_1.default.inspect(schema, { depth: null })); if (schema && (0, openapi_v3_1.isReferenceObject)(schema) && schema.$ref.startsWith('#/components/schemas/')) { const ref = schema.$ref.slice('#/components/schemas/'.length); debug(' referencing:', node_util_1.default.inspect(globalSchemas[ref], { depth: null })); } } if (!schema) return; options = { coerceTypes: !!body.coercionRequired, ...options }; await validateValueAgainstSchema(body.value, schema, globalSchemas, { ...options, source: 'body', }); } exports.validateRequestBody = validateRequestBody; /** * Convert an OpenAPI schema to the corresponding JSON schema. * @param openapiSchema - The OpenAPI schema to convert. */ function convertToJsonSchema(openapiSchema) { const jsonSchema = toJsonSchema(openapiSchema); delete jsonSchema['$schema']; /* istanbul ignore if */ if (debug.enabled) { debug('Converted OpenAPI schema to JSON schema: %s', node_util_1.default.inspect(jsonSchema, { depth: null })); } return jsonSchema; } /** * Built-in cache for complied schemas by AJV */ const DEFAULT_COMPILED_SCHEMA_CACHE = new WeakMap(); /** * Build a cache key for AJV options * @param options - Request body validation options */ function getKeyForOptions(options = ajv_factory_provider_1.DEFAULT_AJV_VALIDATION_OPTIONS) { const ajvOptions = {}; // Sort keys for options const keys = Object.keys(options).sort(); for (const k of keys) { if (k === 'compiledSchemaCache') continue; ajvOptions[k] = options[k]; } return JSON.stringify(ajvOptions); } /** * Validate the value against JSON schema. * @param value - The data value. * @param schema - The JSON schema used to perform the validation. * @param globalSchemas - Schema references. * @param options - Value validation options. */ async function validateValueAgainstSchema( // eslint-disable-next-line @typescript-eslint/no-explicit-any value, schema, globalSchemas = {}, options = {}) { var _a, _b, _c; let validate; const cache = (_a = options.compiledSchemaCache) !== null && _a !== void 0 ? _a : DEFAULT_COMPILED_SCHEMA_CACHE; const key = getKeyForOptions(options); let validatorMap; if (cache.has(schema)) { validatorMap = cache.get(schema); validate = validatorMap.get(key); } if (!validate) { const ajvFactory = (_b = options.ajvFactory) !== null && _b !== void 0 ? _b : new ajv_factory_provider_1.AjvFactoryProvider(options).value(); const ajvInst = ajvFactory(options); validate = createValidator(schema, globalSchemas, ajvInst); validatorMap = validatorMap !== null && validatorMap !== void 0 ? validatorMap : new Map(); validatorMap.set(key, validate); cache.set(schema, validatorMap); } let validationErrors = []; try { const validationResult = validate(value); debug(`Value from ${options.source} passed AJV validation.`, validationResult); return await validationResult; } catch (error) { validationErrors = error.errors; } /* istanbul ignore if */ if (debug.enabled) { debug('Invalid value: %s. Errors: %s', node_util_1.default.inspect(value, { depth: null }), node_util_1.default.inspect(validationErrors)); } if (typeof options.ajvErrorTransformer === 'function') { validationErrors = options.ajvErrorTransformer(validationErrors); } // Throw invalid request body error if (options.source === 'body') { throw __1.RestHttpErrors.invalidRequestBody(buildErrorDetails(validationErrors)); } // Throw invalid value error throw __1.RestHttpErrors.invalidData(value, (_c = options.name) !== null && _c !== void 0 ? _c : '(unknown)', { details: buildErrorDetails(validationErrors), }); } exports.validateValueAgainstSchema = validateValueAgainstSchema; function buildErrorDetails(validationErrors) { return validationErrors.map((e) => { var _a; return { path: e.instancePath, code: e.keyword, message: (_a = e.message) !== null && _a !== void 0 ? _a : `must pass validation rule ${e.keyword}`, info: e.params, }; }); } /** * Create a validate function for the given schema * @param schema - JSON schema for the target * @param globalSchemas - Global schemas * @param ajvInst - An instance of Ajv */ function createValidator(schema, globalSchemas = {}, ajvInst) { const jsonSchema = convertToJsonSchema(schema); // Clone global schemas to set `$async: true` flag const schemas = {}; for (const name in globalSchemas) { // See https://github.com/loopbackio/loopback-next/issues/4939 schemas[name] = { ...globalSchemas[name], $async: true }; } const schemaWithRef = { components: { schemas }, ...jsonSchema }; // See https://js.org/#asynchronous-validation for async validation schemaWithRef.$async = true; return ajvInst.compile(schemaWithRef); } //# sourceMappingURL=request-body.validator.js.map