BRIKEV/openapi-validator-utils

View on GitHub
index.js

Summary

Maintainability
B
6 hrs
Test Coverage
A
100%
const Ajv = require('ajv').default;
const addFormats = require('ajv-formats');
const defaultOptions = require('./config/default');
const {
  formatComponents,
  configError,
  sanitizeValueSchema,
} = require('./utils');
const {
  argsValidation,
  ajvErrors,
  inputValidation,
  endpointValidation,
  optionsValidation,
  responseArgsValidation,
} = require('./validators');

/**
 * @name ValidateRequest
 * @function
 * @param {*} value Value we want to validate
 * @param {string} endpoint OpenApi endpoint we want to validate
 * @param {string} method OpenApi method we want to validate
 * @param {string} contentType Content api of the request we want to validate
 */

/**
 * @name ValidateParams
 * @function
 * @param {*} value Value we want to validate
 * @param {string} endpoint OpenApi endpoint we want to validate
 * @param {string} method OpenApi method we want to validate
 * @param {string} contentType Content api of the request we want to validate
 */

/**
 * @name ValidateResponse
 * @function
 * @param {*} value Value we want to validate
 * @param {string} endpoint OpenApi endpoint we want to validate
 * @param {string} method OpenApi method we want to validate
 * @param {string} status OpenApi status we want to validate
 * @param {string} contentType Content api of the request we want to validate
 */

/**
 * @name IsRequestRequired
 * @function
 * @param {string} endpoint OpenApi endpoint we want to validate
 * @param {string} method OpenApi method we want to validate
 * @param {string} contentType Content api of the request we want to validate
 */

/**
 * @name ValidateRequiredValues
 * @function
 * @param {*} value Values we want to see if are send as required parameters
 * @param {string} endpoint OpenApi endpoint we want to validate
 * @param {string} method OpenApi method we want to validate
 */

/**
 * Validator methods
 * @typedef {object} ValidatorMethods
 * @property {ValidateRequest} validateRequest
 * @property {ValidateParams} validateQueryParam
 * @property {ValidateParams} validatePathParam
 * @property {ValidateParams} validateHeaderParam
 * @property {ValidateResponse} validateResponse
 * @property {IsRequestRequired} isRequestRequired
 * @property {ValidateRequiredValues} validateRequiredValues
 */

/**
 * Validate method
 * @param {object} openApiDef OpenAPI definition
 * @param {object} options Options to extend the errorHandler or Ajv configuration
 * @returns {ValidatorMethods} validator methods
 */
const validate = (openApiDef, userOptions = {}) => {
  const options = {
    ...defaultOptions,
    ...(userOptions || {}),
  };

  const { errorHandler } = options;
  const inputValidationError = inputValidation(openApiDef);
  configError(inputValidationError, errorHandler);
  const optionsValidationError = optionsValidation(userOptions);
  configError(optionsValidationError, errorHandler);

  const schemaOptions = { strictValidation: options.strictValidation };
  const defsSchema = {
    $id: 'defs.json',
    definitions: {
      components: formatComponents(openApiDef.components, schemaOptions),
    },
  };
  const ajv = new Ajv({
    schemas: [defsSchema],
    ...(options.ajvConfig || {}),
  });
  addFormats(ajv);

  // Obtain method to validate against a given schema
  const getSchemaValidator = (schemaName, schema) => {
    // Slashes (/) are not supported in schema names, so we must replace them
    const validSchemaName = schemaName.replace(/\//g, '-');

    let validateSchema = ajv.getSchema(validSchemaName);

    // Compile schema just once, only if it hasn't been defined previously
    if (!validateSchema) {
      ajv.addSchema(schema, validSchemaName);
      validateSchema = ajv.getSchema(validSchemaName);
    }

    return validateSchema;
  };

  // Validate is current value matches the structure in the selected schema
  const schemaValidation = (value, schema, type, schemaName) => {
    const validateSchema = getSchemaValidator(schemaName, schema);
    const valid = validateSchema(value);
    if (!valid) {
      return ajvErrors(validateSchema.errors, value, type, errorHandler);
    }
    return true;
  };

  const validateRequiredValues = (values, endpoint, method) => {
    const argsValidationError = argsValidation(values, endpoint, method);
    configError(argsValidationError, errorHandler);
    const requiredParamsError = endpointValidation.requiredParams(
      values,
      openApiDef,
      endpoint,
      method,
    );
    configError(requiredParamsError, errorHandler);
    return true;
  };

  const validateResponse = (value, endpoint, method, status, contentType = 'application/json') => {
    const argsValidationError = responseArgsValidation(value, endpoint, method, status);
    configError(argsValidationError, errorHandler);
    const responseEndpoint = endpointValidation.response(
      openApiDef,
      endpoint,
      method,
      status,
      contentType,
    );
    configError(responseEndpoint, errorHandler);
    let responseSchema = {
      ...openApiDef.paths[endpoint][method].responses[status].content[contentType].schema,
    };
    responseSchema = formatComponents(responseSchema);
    const schemaName = `${method}-${endpoint}-${status}-${contentType}-response`;
    return schemaValidation(value, responseSchema, 'response', schemaName);
  };

  const isRequestRequired = (endpoint, method, contentType = 'application/json') => {
    try {
      if (method === 'get') return false;
      const argsValidationError = argsValidation('request', endpoint, method);
      configError(argsValidationError, errorHandler);
      const requestEndpoint = endpointValidation.request(openApiDef, endpoint, method, contentType);
      configError(requestEndpoint, errorHandler);
      return !!openApiDef.paths[endpoint][method].requestBody.required;
    } catch (error) {
      // When we receive this is because it was not documented
      // Document request body it is not required there might be endpoints
      // where you don't want request body
      if (error.message.includes('does not have requestBody definition')) return false;
      throw error;
    }
  };

  const validateRequest = (value, endpoint, method, contentType = 'application/json') => {
    const argsValidationError = argsValidation(value, endpoint, method);
    configError(argsValidationError, errorHandler);
    const requestEndpoint = endpointValidation.request(openApiDef, endpoint, method, contentType);
    configError(requestEndpoint, errorHandler);
    let requestBodySchema = {
      ...openApiDef.paths[endpoint][method].requestBody.content[contentType].schema,
    };
    requestBodySchema = formatComponents(requestBodySchema);
    const schemaName = `${method}-${endpoint}-${contentType}-request`;
    return schemaValidation(value, requestBodySchema, 'request', schemaName);
  };

  const validateParam = type => (value, key, endpoint, method) => {
    const argsValidationError = argsValidation(value, endpoint, method, key);
    configError(argsValidationError, errorHandler);
    const paramEndpoint = endpointValidation.params(openApiDef, endpoint, method, key, type);
    configError(paramEndpoint, errorHandler);
    let parametersSchema = paramEndpoint.parameter.schema;
    parametersSchema = formatComponents(parametersSchema);
    const sanitizeValue = sanitizeValueSchema(value, parametersSchema);
    const schemaName = `${method}-${endpoint}-${key}-${type}-param`;
    return schemaValidation(sanitizeValue, parametersSchema, type, schemaName);
  };

  return {
    validateRequest,
    validateQueryParam: validateParam('query'),
    validatePathParam: validateParam('path'),
    validateHeaderParam: validateParam('header'),
    validateResponse,
    validateRequiredValues,
    isRequestRequired,
  };
};

module.exports = validate;