openapi-request-validator
Version:
Validate request properties against an OpenAPI spec.
572 lines • 23.2 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
exports.__esModule = true;
var ajv_1 = require("ajv");
var ajv_formats_1 = require("ajv-formats");
var openapi_jsonschema_parameters_1 = require("openapi-jsonschema-parameters");
var ts_log_1 = require("ts-log");
var contentTypeParser = require('content-type');
var LOCAL_DEFINITION_REGEX = /^#\/([^\/]+)\/([^\/]+)$/;
var OpenAPIRequestValidator = /** @class */ (function () {
function OpenAPIRequestValidator(args) {
var _this = this;
this.logger = ts_log_1.dummyLogger;
this.loggingKey = '';
this.requestBodyValidators = {};
this.enableHeadersLowercase = true;
var loggingKey = args && args.loggingKey ? args.loggingKey + ': ' : '';
this.loggingKey = loggingKey;
if (!args) {
throw new Error("".concat(loggingKey, "missing args argument"));
}
if (args.logger) {
this.logger = args.logger;
}
if (args.hasOwnProperty('enableHeadersLowercase')) {
this.enableHeadersLowercase = args.enableHeadersLowercase;
}
var errorTransformer = typeof args.errorTransformer === 'function' && args.errorTransformer;
var errorMapper = errorTransformer
? extendedErrorMapper(errorTransformer)
: toOpenapiValidationError;
var bodyValidationSchema;
var bodySchema;
var headersSchema;
var formDataSchema;
var pathSchema;
var querySchema;
var isBodyRequired;
if (args.parameters !== undefined) {
if (Array.isArray(args.parameters)) {
var schemas = (0, openapi_jsonschema_parameters_1.convertParametersToJSONSchema)(args.parameters);
bodySchema = schemas.body;
headersSchema = lowercasedHeaders(schemas.headers, this.enableHeadersLowercase);
formDataSchema = schemas.formData;
pathSchema = schemas.path;
if (schemas.query && args.hasOwnProperty('additionalQueryProperties')) {
schemas.query.additionalProperties = args.additionalQueryProperties;
}
querySchema = schemas.query;
isBodyRequired =
// @ts-ignore
args.parameters.filter(byRequiredBodyParameters).length > 0;
}
else {
throw new Error("".concat(loggingKey, "args.parameters must be an Array"));
}
}
var v = new ajv_1["default"](__assign({ useDefaults: true, allErrors: true, strict: false,
// @ts-ignore TODO get Ajv updated to account for logger
logger: false }, (args.ajvOptions || {})));
(0, ajv_formats_1["default"])(v);
v.removeKeyword('readOnly');
v.addKeyword({
keyword: 'readOnly',
modifying: true,
compile: function (sch) {
if (sch) {
return function validate(data, dataCtx) {
validate.errors = [
{
keyword: 'readOnly',
instancePath: dataCtx.instancePath,
message: 'is read-only',
params: { readOnly: dataCtx.parentDataProperty }
},
];
return !(sch === true && data !== null);
};
}
return function () { return true; };
}
});
if (args.requestBody) {
isBodyRequired = args.requestBody.required || false;
}
if (args.customFormats) {
var hasNonFunctionProperty_1;
Object.keys(args.customFormats).forEach(function (format) {
var func = args.customFormats[format];
if (typeof func === 'function') {
v.addFormat(format, func);
}
else {
hasNonFunctionProperty_1 = true;
}
});
if (hasNonFunctionProperty_1) {
throw new Error("".concat(loggingKey, "args.customFormats properties must be functions"));
}
}
if (args.customKeywords) {
for (var _i = 0, _a = Object.entries(args.customKeywords); _i < _a.length; _i++) {
var _b = _a[_i], keywordName = _b[0], keywordDefinition = _b[1];
v.addKeyword(__assign({ keyword: keywordName }, keywordDefinition));
}
}
if (bodySchema) {
bodyValidationSchema = {
properties: {
body: bodySchema
}
};
}
if (args.componentSchemas) {
// openapi v3:
Object.keys(args.componentSchemas).forEach(function (id) {
v.addSchema(args.componentSchemas[id], "#/components/schemas/".concat(id));
_this.addSchemaProperties(v, args.componentSchemas[id], "#/components/schemas/".concat(id));
});
}
else if (args.schemas) {
if (Array.isArray(args.schemas)) {
args.schemas.forEach(function (schema) {
var id = schema.id;
if (id) {
var localSchemaPath = LOCAL_DEFINITION_REGEX.exec(id);
if (localSchemaPath && bodyValidationSchema) {
var definitions = bodyValidationSchema[localSchemaPath[1]];
if (!definitions) {
definitions = bodyValidationSchema[localSchemaPath[1]] = {};
}
definitions[localSchemaPath[2]] = schema;
}
// backwards compatibility with json-schema-draft-04
delete schema.id;
v.addSchema(__assign({ $id: id }, schema), id);
}
else {
_this.logger.warn(loggingKey, 'igorning schema without id property');
}
});
}
else if (bodySchema) {
bodyValidationSchema.definitions = args.schemas;
bodyValidationSchema.components = {
schemas: args.schemas
};
}
}
if (args.externalSchemas) {
Object.keys(args.externalSchemas).forEach(function (id) {
v.addSchema(args.externalSchemas[id], id);
});
}
if (args.requestBody) {
/* tslint:disable-next-line:forin */
for (var mediaTypeKey in args.requestBody.content) {
var bodyContentSchema = args.requestBody.content[mediaTypeKey].schema;
var copied = JSON.parse(JSON.stringify(bodyContentSchema));
var resolvedSchema = resolveAndSanitizeRequestBodySchema(copied, v);
this.requestBodyValidators[mediaTypeKey] = v.compile(transformOpenAPIV3Definitions({
properties: {
body: resolvedSchema
},
definitions: args.schemas || {},
components: { schemas: args.schemas }
}));
}
}
this.bodySchema = bodySchema;
this.errorMapper = errorMapper;
this.isBodyRequired = isBodyRequired;
this.requestBody = args.requestBody;
this.validateBody =
bodyValidationSchema &&
v.compile(transformOpenAPIV3Definitions(bodyValidationSchema));
this.validateFormData =
formDataSchema &&
v.compile(transformOpenAPIV3Definitions(formDataSchema));
this.validateHeaders =
headersSchema && v.compile(transformOpenAPIV3Definitions(headersSchema));
this.validatePath =
pathSchema && v.compile(transformOpenAPIV3Definitions(pathSchema));
this.validateQuery =
querySchema && v.compile(transformOpenAPIV3Definitions(querySchema));
}
OpenAPIRequestValidator.prototype.validateRequest = function (request) {
var errors = [];
var err;
var schemaError;
var mediaTypeError;
if (this.bodySchema) {
if (request.body) {
if (!this.validateBody({ body: request.body })) {
errors.push.apply(errors, withAddedLocation('body', this.validateBody.errors));
}
}
else if (this.isBodyRequired) {
schemaError = {
location: 'body',
message: 'request.body was not present in the request. Is a body-parser being used?',
schema: this.bodySchema
};
}
}
if (this.requestBody) {
var contentType = getHeaderValue(request.headers, 'content-type');
var mediaTypeMatch = getSchemaForMediaType(contentType, this.requestBody, this.logger, this.loggingKey);
if (!mediaTypeMatch) {
if (contentType) {
mediaTypeError = {
message: "Unsupported Content-Type ".concat(contentType)
};
}
else if (this.isBodyRequired) {
errors.push({
keyword: 'required',
instancePath: '/body',
params: {},
message: 'media type is not specified',
location: 'body'
});
}
}
else {
var bodySchema = this.requestBody.content[mediaTypeMatch].schema;
if (request.body) {
var validateBody = this.requestBodyValidators[mediaTypeMatch];
if (!validateBody({ body: request.body })) {
errors.push.apply(errors, withAddedLocation('body', validateBody.errors));
}
}
else if (this.isBodyRequired) {
schemaError = {
location: 'body',
message: 'request.body was not present in the request. Is a body-parser being used?',
schema: bodySchema
};
}
}
}
if (this.validateFormData && !schemaError) {
if (!this.validateFormData(request.body)) {
errors.push.apply(errors, withAddedLocation('formData', this.validateFormData.errors));
}
}
if (this.validatePath) {
if (!this.validatePath(request.params || {})) {
errors.push.apply(errors, withAddedLocation('path', this.validatePath.errors));
}
}
if (this.validateHeaders) {
if (!this.validateHeaders(lowercaseRequestHeaders(request.headers || {}, this.enableHeadersLowercase))) {
errors.push.apply(errors, withAddedLocation('headers', this.validateHeaders.errors));
}
}
if (this.validateQuery) {
if (!this.validateQuery(request.query || {})) {
errors.push.apply(errors, withAddedLocation('query', this.validateQuery.errors));
}
}
if (errors.length) {
err = {
status: 400,
errors: errors.map(this.errorMapper)
};
}
else if (schemaError) {
err = {
status: 400,
errors: [schemaError]
};
}
else if (mediaTypeError) {
err = {
status: 415,
errors: [mediaTypeError]
};
}
return err;
};
OpenAPIRequestValidator.prototype.validate = function (request) {
console.warn('validate is deprecated, use validateRequest instead.');
this.validateRequest(request);
};
OpenAPIRequestValidator.prototype.addSchemaProperties = function (v, schema, prefix) {
for (var attr in schema) {
if (schema.hasOwnProperty(attr)) {
switch (attr) {
case 'allOf':
case 'oneOf':
case 'anyOf':
for (var i = 0; i < schema[attr].length; i++) {
this.addSchemaProperties(v, schema[attr][i], "".concat(prefix, "/").concat(attr, "/").concat(i));
}
return;
case 'items':
this.addSchemaProperties(v, schema[attr], "".concat(prefix, "/").concat(attr));
return;
case 'properties':
for (var propertyId in schema[attr]) {
if (schema[attr].hasOwnProperty(propertyId)) {
var schemaId = "".concat(prefix, "/").concat(attr, "/").concat(propertyId);
v.addSchema(schema[attr][propertyId], schemaId);
this.addSchemaProperties(v, schema[attr][propertyId], schemaId);
}
}
return;
}
}
}
};
return OpenAPIRequestValidator;
}());
exports["default"] = OpenAPIRequestValidator;
function byRequiredBodyParameters(param) {
// @ts-ignore
return (param["in"] === 'body' || param["in"] === 'formData') && param.required;
}
function extendedErrorMapper(mapper) {
return function (ajvError) { return mapper(toOpenapiValidationError(ajvError), ajvError); };
}
function getSchemaForMediaType(contentTypeHeader, requestBodySpec, logger, loggingKey) {
if (!contentTypeHeader) {
return;
}
var contentType;
try {
contentType = contentTypeParser.parse(contentTypeHeader).type;
}
catch (e) {
logger.warn(loggingKey, 'failed to parse content-type', contentTypeHeader, e);
if (e instanceof TypeError && e.message === 'invalid media type') {
return;
}
throw e;
}
var content = requestBodySpec.content;
var subTypeWildCardPoints = 2;
var wildcardMatchPoints = 1;
var match;
var matchPoints = 0;
for (var mediaTypeKey in content) {
if (content.hasOwnProperty(mediaTypeKey)) {
if (mediaTypeKey.indexOf(contentType) > -1) {
return mediaTypeKey;
}
else if (mediaTypeKey === '*/*' && wildcardMatchPoints > matchPoints) {
match = mediaTypeKey;
matchPoints = wildcardMatchPoints;
}
var contentTypeParts = contentType.split('/');
var mediaTypeKeyParts = mediaTypeKey.split('/');
if (mediaTypeKeyParts[1] !== '*') {
continue;
}
else if (contentTypeParts[0] === mediaTypeKeyParts[0] &&
subTypeWildCardPoints > matchPoints) {
match = mediaTypeKey;
matchPoints = subTypeWildCardPoints;
}
}
}
return match;
}
function lowercaseRequestHeaders(headers, enableHeadersLowercase) {
if (enableHeadersLowercase) {
var lowerCasedHeaders_1 = {};
Object.keys(headers).forEach(function (header) {
lowerCasedHeaders_1[header.toLowerCase()] = headers[header];
});
return lowerCasedHeaders_1;
}
else {
return headers;
}
}
function lowercasedHeaders(headersSchema, enableHeadersLowercase) {
if (headersSchema && enableHeadersLowercase) {
var properties_1 = headersSchema.properties;
Object.keys(properties_1).forEach(function (header) {
var property = properties_1[header];
delete properties_1[header];
properties_1[header.toLowerCase()] = property;
});
if (headersSchema.required && headersSchema.required.length) {
headersSchema.required = headersSchema.required.map(function (header) {
return header.toLowerCase();
});
}
}
return headersSchema;
}
function toOpenapiValidationError(error) {
var validationError = {
path: 'instance' + error.instancePath,
errorCode: "".concat(error.keyword, ".openapi.requestValidation"),
message: error.message,
location: error.location
};
if (error.keyword === '$ref') {
delete validationError.errorCode;
validationError.schema = { $ref: error.params.ref };
}
if (error.params.missingProperty) {
validationError.path += '/' + error.params.missingProperty;
}
validationError.path = validationError.path.replace(error.location === 'body' ? /^instance\/body\/?/ : /^instance\/?/, '');
validationError.path = validationError.path.replace(/\//g, '.');
if (!validationError.path) {
delete validationError.path;
}
return stripBodyInfo(validationError);
}
function stripBodyInfo(error) {
if (error.location === 'body') {
if (typeof error.path === 'string') {
error.path = error.path.replace(/^body\./, '');
}
else {
// Removing to avoid breaking clients that are expecting strings.
delete error.path;
}
error.message = error.message.replace(/^instance\.body\./, 'instance.');
}
return error;
}
function withAddedLocation(location, errors) {
errors.forEach(function (error) {
error.location = location;
});
return errors;
}
function resolveAndSanitizeRequestBodySchema(requestBodySchema, v) {
var resolved;
var copied;
if ('properties' in requestBodySchema) {
var schema_1 = requestBodySchema;
Object.keys(schema_1.properties).forEach(function (property) {
var prop = schema_1.properties[property];
prop = sanitizeReadonlyPropertiesFromRequired(prop);
if (!prop.hasOwnProperty('$ref') && !prop.hasOwnProperty('items')) {
prop = resolveAndSanitizeRequestBodySchema(prop, v);
}
});
requestBodySchema = sanitizeReadonlyPropertiesFromRequired(requestBodySchema);
}
else if ('$ref' in requestBodySchema) {
resolved = v.getSchema(requestBodySchema.$ref);
if (resolved && resolved.schema) {
copied = JSON.parse(JSON.stringify(resolved.schema));
copied = sanitizeReadonlyPropertiesFromRequired(copied);
copied = resolveAndSanitizeRequestBodySchema(copied, v);
requestBodySchema = copied;
}
}
else if ('items' in requestBodySchema) {
if ('$ref' in requestBodySchema.items) {
resolved = v.getSchema(requestBodySchema.items.$ref);
if (resolved && resolved.schema) {
copied = JSON.parse(JSON.stringify(resolved.schema));
copied = sanitizeReadonlyPropertiesFromRequired(copied);
copied = resolveAndSanitizeRequestBodySchema(copied, v);
requestBodySchema.items = copied;
}
}
}
else if ('allOf' in requestBodySchema) {
requestBodySchema.allOf = requestBodySchema.allOf.map(function (val) {
val = sanitizeReadonlyPropertiesFromRequired(val);
return resolveAndSanitizeRequestBodySchema(val, v);
});
}
else if ('oneOf' in requestBodySchema) {
requestBodySchema.oneOf = requestBodySchema.oneOf.map(function (val) {
val = sanitizeReadonlyPropertiesFromRequired(val);
return resolveAndSanitizeRequestBodySchema(val, v);
});
}
else if ('anyOf' in requestBodySchema) {
requestBodySchema.anyOf = requestBodySchema.anyOf.map(function (val) {
val = sanitizeReadonlyPropertiesFromRequired(val);
return resolveAndSanitizeRequestBodySchema(val, v);
});
}
return requestBodySchema;
}
function sanitizeReadonlyPropertiesFromRequired(schema) {
if ('properties' in schema && 'required' in schema) {
var readOnlyProps = Object.keys(schema.properties).map(function (key) {
var prop = schema.properties[key];
if (prop && 'readOnly' in prop) {
if (prop.readOnly === true) {
return key;
}
}
return;
});
readOnlyProps
.filter(function (i) { return i !== undefined; })
.forEach(function (value) {
var index = schema.required.indexOf(value);
if (index !== -1) {
schema.required.splice(index, 1);
}
});
}
return schema;
}
function recursiveTransformOpenAPIV3Definitions(object) {
// Transformations //
// OpenAPIV3 nullable
if (object.nullable === true) {
if (object["enum"]) {
// Enums can not be null with type null
object.oneOf = [
{ type: 'null' },
{
type: object.type,
"enum": object["enum"]
},
];
delete object.type;
delete object["enum"];
}
else if (object.type) {
object.type = [object.type, 'null'];
}
else if (object.allOf) {
object.anyOf = [{ allOf: object.allOf }, { type: 'null' }];
delete object.allOf;
}
else if (object.oneOf || object.anyOf) {
var arr = object.oneOf || object.anyOf;
arr.push({ type: 'null' });
}
delete object.nullable;
}
Object.keys(object).forEach(function (attr) {
if (typeof object[attr] === 'object' && object[attr] !== null) {
recursiveTransformOpenAPIV3Definitions(object[attr]);
}
else if (Array.isArray(object[attr])) {
object[attr].forEach(function (obj) {
return recursiveTransformOpenAPIV3Definitions(obj);
});
}
});
}
function transformOpenAPIV3Definitions(schema) {
if (typeof schema !== 'object') {
return schema;
}
var res = JSON.parse(JSON.stringify(schema));
recursiveTransformOpenAPIV3Definitions(res);
return res;
}
function getHeaderValue(requestHeaders, header) {
var matchingHeaders = Object.keys(requestHeaders).filter(function (key) { return key.toLowerCase() === header.toLowerCase(); });
return requestHeaders[matchingHeaders[0]];
}
//# sourceMappingURL=index.js.map