express-openapi-validate
Version:
Express middleware to validate request based on an OpenAPI 3 document
221 lines (218 loc) • 10.2 kB
JavaScript
;
/*
Copyright 2018 Santeri Hiltunen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const ajv_1 = __importDefault(require("ajv"));
const ajv_formats_1 = __importDefault(require("ajv-formats"));
const lodash_1 = __importDefault(require("lodash"));
const path_to_regexp_1 = require("path-to-regexp");
const semver = __importStar(require("semver"));
const debug_1 = __importDefault(require("./debug"));
const formats = __importStar(require("./formats"));
const parameters = __importStar(require("./parameters"));
const schema_utils_1 = require("./schema-utils");
const ValidationError_1 = __importDefault(require("./ValidationError"));
const resolveResponse = (res) => {
if (res == null) {
throw new TypeError(`Response was ${String(res)}`);
}
const statusCodeNum = Number(res.statusCode || res.status);
const statusCode = Number.isNaN(statusCodeNum) ? null : statusCodeNum;
const body = res.body || res.data;
const { headers } = res;
if (statusCode == null || body == null || headers == null) {
throw new TypeError("statusCode, body or header values not found from response");
}
return { statusCode, body, headers };
};
class OpenApiValidator {
constructor(openApiDocument, options = {}) {
if (!semver.satisfies(openApiDocument.openapi, "^3.0.0")) {
const version = openApiDocument.openapi || openApiDocument.swagger;
throw new Error(`Unsupported OpenAPI / Swagger version=${version}`);
}
this._document = openApiDocument;
const userAjvFormats = lodash_1.default.get(options, ["ajvOptions", "formats"], {});
const ajvOptions = {
discriminator: true,
...options.ajvOptions,
formats: { ...formats, ...userAjvFormats },
};
this._ajv = new ajv_1.default(ajvOptions);
ajv_formats_1.default(this._ajv, ["date", "date-time"]);
this._ajv.addKeyword("example");
this._ajv.addKeyword("xml");
this._ajv.addKeyword("externalDocs");
}
validate(method, path) {
const pathItemObject = this._getPathItemObject(path);
const operation = this._getOperationObject(method, path);
const requestBodyObject = schema_utils_1.resolveReference(this._document, lodash_1.default.get(operation, ["requestBody"], {}));
const bodySchema = lodash_1.default.get(requestBodyObject, ["content", "application/json", "schema"], {});
const params = parameters.resolve(this._document, pathItemObject.parameters, operation.parameters);
const parametersSchema = parameters.buildSchema(params);
const schema = {
type: "object",
properties: {
body: schema_utils_1.resolveReference(this._document, bodySchema),
...parametersSchema,
},
required: ["query", "headers", "params"],
};
if (!lodash_1.default.isEmpty(parametersSchema.cookies)) {
schema.required.push("cookies");
}
if (lodash_1.default.get(requestBodyObject, ["required"]) === true) {
schema.required.push("body");
}
const jsonSchema = schema_utils_1.mapOasSchemaToJsonSchema(schema, this._document);
const validator = this._ajv.compile(jsonSchema);
debug_1.default(`Request JSON Schema for ${method} ${path}: %j`, jsonSchema);
const validate = (req, res, next) => {
const reqToValidate = {
...lodash_1.default.pick(req, "query", "headers", "params", "body"),
cookies: req.cookies
? { ...req.cookies, ...req.signedCookies }
: undefined,
};
const valid = validator(reqToValidate);
if (valid) {
next();
}
else {
const errors = validator.errors;
const errorText = this._ajv.errorsText(errors, { dataVar: "request" });
const err = new ValidationError_1.default(`Error while validating request: ${errorText}`, errors);
next(err);
}
};
return validate;
}
match(options = { allowNoMatch: false }) {
const paths = lodash_1.default.keys(this._document.paths).map((path) => ({
path,
regex: path_to_regexp_1.pathToRegexp(schema_utils_1.oasPathToExpressPath(path)),
}));
const matchAndValidate = (req, res, next) => {
const match = paths.find(({ regex }) => regex.test(req.path));
const method = req.method.toLowerCase();
if (match) {
this.validate(method, match.path)(req, res, next);
}
else if (!options.allowNoMatch) {
const err = new Error(`Path=${req.path} with method=${method} not found from OpenAPI document`);
next(err);
}
else {
// match not required
next();
}
};
return matchAndValidate;
}
validateResponse(method, path) {
const operation = this._getOperationObject(method, path);
const validateResponse = (userResponse) => {
const { statusCode, ...response } = resolveResponse(userResponse);
const responseObject = this._getResponseObject(operation, statusCode);
const bodySchema = lodash_1.default.get(responseObject, ["content", "application/json", "schema"], {});
const headerObjectMap = lodash_1.default.get(responseObject, ["headers"], {});
const headersSchema = {
type: "object",
properties: {},
};
Object.keys(headerObjectMap).forEach((key) => {
const headerObject = schema_utils_1.resolveReference(this._document, headerObjectMap[key]);
const name = key.toLowerCase();
if (name === "content-type") {
return;
}
if (headerObject.required === true) {
if (!Array.isArray(headersSchema.required)) {
headersSchema.required = [];
}
headersSchema.required.push(name);
}
headersSchema.properties[name] = schema_utils_1.resolveReference(this._document, headerObject.schema || {});
});
const schema = schema_utils_1.mapOasSchemaToJsonSchema({
type: "object",
properties: {
body: schema_utils_1.resolveReference(this._document, bodySchema),
headers: headersSchema,
},
required: ["headers", "body"],
}, this._document);
debug_1.default(`Response JSON Schema for ${method} ${path} ${statusCode}: %j`, schema);
const valid = this._ajv.validate(schema, response);
if (!valid) {
const errorText = this._ajv.errorsText(this._ajv.errors, {
dataVar: "response",
});
throw new ValidationError_1.default(`Error while validating response: ${errorText}`, this._ajv.errors);
}
};
return validateResponse;
}
_getResponseObject(op, statusCode) {
const statusCodeStr = String(statusCode);
let responseObject = lodash_1.default.get(op, ["responses", statusCodeStr], null);
if (responseObject === null) {
const field = `${statusCodeStr[0]}XX`;
responseObject = lodash_1.default.get(op, ["responses", field], null);
}
if (responseObject === null) {
responseObject = lodash_1.default.get(op, ["responses", "default"], null);
}
if (responseObject === null) {
throw new Error(`No response object found with statusCode=${statusCodeStr}`);
}
return schema_utils_1.resolveReference(this._document, responseObject);
}
_getPathItemObject(path) {
if (lodash_1.default.has(this._document, ["paths", path])) {
return this._document.paths[path];
}
throw new Error(`Path=${path} not found from OpenAPI document`);
}
_getOperationObject(method, path) {
if (lodash_1.default.has(this._document, ["paths", path, method])) {
return this._document.paths[path][method];
}
throw new Error(`Path=${path} with method=${method} not found from OpenAPI document`);
}
}
exports.default = OpenApiValidator;
//# sourceMappingURL=OpenApiValidator.js.map