UNPKG

recoder-code

Version:

🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!

257 lines • 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ResponseValidator = void 0; const modded_express_mung_1 = require("../framework/modded.express.mung"); const ajv_1 = require("../framework/ajv"); const util_1 = require("./util"); const types_1 = require("../framework/types"); const mediaTypeParser = require("media-typer"); const contentTypeParser = require("content-type"); class ResponseValidator { constructor(openApiSpec, options = {}, eovOptions = {}, serial = -1) { this.validatorsCache = {}; this.spec = openApiSpec; this.ajvBody = (0, ajv_1.createResponseAjv)(openApiSpec, options); this.eovOptions = eovOptions; this.serial = serial; // This is a pseudo-middleware function. It doesn't get registered with // express via `use` modded_express_mung_1.default.onError = (err, req, res, next) => { return next(err); }; } validate() { return modded_express_mung_1.default.json((body, req, res) => { var _a; if (req.openapi && this.serial == req.openapi.serial) { const openapi = req.openapi; // instead of openapi.schema, use openapi._responseSchema to get the response copy const responses = (_a = openapi ._responseSchema) === null || _a === void 0 ? void 0 : _a.responses; const validators = this._getOrBuildValidator(req, responses); const path = req.originalUrl; const statusCode = res.statusCode; const contentType = res.getHeaders()['content-type']; const accept = req.headers['accept']; // ir response has a content type use it, else use accept headers const accepts = contentType ? [contentType] : accept ? accept.split(',').map((h) => h.trim()) : []; try { return this._validate({ validators, body, statusCode, path, accepts, // return 406 if not acceptable }); } catch (err) { // If a custom error handler was provided, we call that if (err instanceof types_1.InternalServerError && this.eovOptions.onError) { this.eovOptions.onError(err, body, req); } else { // No custom error handler, or something unexpected happen. throw err; } } } return body; }); } // TODO public for test only - fix me // Build validators for each url/method/contenttype tuple _getOrBuildValidator(req, responses) { var _a; // get the request content type - used only to build the cache key const contentTypeMeta = util_1.ContentType.from(req); const contentType = (_a = contentTypeMeta.normalize()) !== null && _a !== void 0 ? _a : 'not_provided'; const openapi = req.openapi; const key = `${req.method}-${openapi.expressRoute}-${contentType}`; let validators = this.validatorsCache[key]; if (!validators) { validators = this.buildValidators(responses, key); this.validatorsCache[key] = validators; } return validators; } // TODO public for test only - fix me _validate({ validators, body, statusCode, path, accepts, // optional }) { const status = statusCode !== null && statusCode !== void 0 ? statusCode : 'default'; const statusXX = status.toString()[0] + 'XX'; let svalidator; if (status in validators) { svalidator = validators[status]; } else if (statusXX in validators) { svalidator = validators[statusXX]; } else if (validators.default) { svalidator = validators.default; } else { throw new types_1.InternalServerError({ path: path, message: `no schema defined for status code '${status}' in the openapi spec`, }); } const validatorContentTypes = Object.keys(svalidator); const contentType = (0, util_1.findResponseContent)(accepts, validatorContentTypes) || validatorContentTypes[0]; // take first contentType, if none found if (validatorContentTypes.length === 0) { // spec specifies no content for this response if (body !== undefined) { // response contains content/body throw new types_1.InternalServerError({ path: '/response', message: 'response should NOT have a body', }); } // response contains no content/body so OK return; } if (!contentType) { // not contentType inferred, assume valid console.warn('no contentType found'); return; } const validator = svalidator[contentType]; if (!validator) { // no validator found, assume valid console.warn('no validator found'); return; } if (body === undefined || body === null) { throw new types_1.InternalServerError({ path: '/response', message: 'response body required.', }); } // CHECK If Content-Type is validatable try { if (!this.canValidateContentType(contentType)) { console.warn('Cannot validate content type', contentType); // assume valid return; } } catch (e) { // Do nothing. Move on and validate response } const valid = validator({ response: body, }); if (!valid) { const errors = (0, util_1.augmentAjvErrors)(validator.errors); const message = this.ajvBody.errorsText(errors, { dataVar: '', // responses }); throw new types_1.InternalServerError({ path: path, errors: (0, util_1.ajvErrorsToValidatorError)(500, errors).errors, message: message, }); } } /** * Build a map of response name to response validator, for the set of responses * defined on the current endpoint * @param responses * @returns a map of validators */ buildValidators(responses, ajvCacheKey) { var _a, _b, _c; const validationTypes = (response) => { if (!response.content) { return ['no_content']; } if (typeof response.content !== 'object') { return []; } const types = []; for (let contentType of Object.keys(response.content)) { try { if (this.canValidateContentType(contentType)) { if (response.content[contentType] && response.content[contentType].schema) { types.push(contentType); } } } catch (e) { // Handle wildcards if (response.content[contentType].schema && (contentType === '*/*' || new RegExp(/^[a-z]+\/\*$/).test(contentType))) { types.push(contentType); } } } return types; }; const responseSchemas = {}; for (const [name, resp] of Object.entries(responses)) { let tmpResponse = resp; if (tmpResponse.$ref) { // resolve top level response $ref const id = tmpResponse.$ref.replace(/^.+\//i, ''); tmpResponse = (_b = (_a = this.spec.components) === null || _a === void 0 ? void 0 : _a.responses) === null || _b === void 0 ? void 0 : _b[id]; } const response = tmpResponse; const types = validationTypes(response); for (const mediaTypeToValidate of types) { if (!mediaTypeToValidate) { // TODO support content other than JSON // don't validate // assume is valid continue; } else if (mediaTypeToValidate === 'no_content') { responseSchemas[name] = {}; continue; } const schema = response.content[mediaTypeToValidate].schema; responseSchemas[name] = Object.assign(Object.assign({}, responseSchemas[name]), { [mediaTypeToValidate]: { // $schema: 'http://json-schema.org/schema#', // $schema: "http://json-schema.org/draft-04/schema#", type: 'object', properties: { response: schema, }, components: (_c = this.spec.components) !== null && _c !== void 0 ? _c : {}, } }); } } const validators = {}; for (const [code, contentTypeSchemas] of Object.entries(responseSchemas)) { if (Object.keys(contentTypeSchemas).length === 0) { validators[code] = {}; } for (const contentType of Object.keys(contentTypeSchemas)) { const schema = contentTypeSchemas[contentType]; schema.paths = this.spec.paths; // add paths for resolution with multi-file schema.components = this.spec.components; // add components for resolution w/ multi-file const validator = (0, util_1.useAjvCache)(this.ajvBody, schema, `${ajvCacheKey}-${code}-${contentType}`); validators[code] = Object.assign(Object.assign({}, validators[code]), { [contentType]: validator }); } } return validators; } /** * Checks if specific Content-Type is validatable * @param contentType * @returns boolean * @throws error on invalid content type format */ canValidateContentType(contentType) { const contentTypeParsed = contentTypeParser.parse(contentType); const mediaTypeParsed = mediaTypeParser.parse(contentTypeParsed.type); return (mediaTypeParsed.subtype === 'json' || mediaTypeParsed.suffix === 'json'); } } exports.ResponseValidator = ResponseValidator; //# sourceMappingURL=openapi.response.validator.js.map