MitocGroup/deep-framework

View on GitHub
src/deep-validation/lib/Validation.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * Created by AlexanderC on 6/15/15.
 */

'use strict';

import Kernel from 'deep-kernel';
import {ObjectToJoi} from './ObjectToJoi';
import {ValidationSchemaNotFoundException} from './Exception/ValidationSchemaNotFoundException';
import {InvalidJoiSchemaException} from './Exception/InvalidJoiSchemaException';
import Joi from 'joi';
import {ObjectValidationFailedException} from './Exception/ObjectValidationFailedException';
import path from 'path';
import Core from 'deep-core';

/**
 * Validation engine
 */
export class Validation extends Kernel.ContainerAware {
  /**
   * @param {Array} models
   * @param {Boolean} forcePartitionField
   * @param {String[]} nonPartitionedModels
   */
  constructor(models = [], forcePartitionField = false, nonPartitionedModels = []) {
    super();

    this._forcePartitionField = forcePartitionField;
    this._nonPartitionedModels = nonPartitionedModels || [];

    this._schemas = this._rawModelsToSchemas(models);
  }

  /**
   * @param {Runtime} lambdaRuntime
   * @param {String} schemaName
   * @param {Function} cb
   * @returns {Validation}
   */
  validateRuntimeInput(lambdaRuntime, schemaName, cb) {
    if (!this.hasSchema(schemaName)) {
      lambdaRuntime.createError(new ValidationSchemaNotFoundException(schemaName)).send();
      return this;
    }

    let validationResult = this.validate(schemaName, lambdaRuntime.request.data, true);

    if (!validationResult.error) {
      cb(validationResult.value);
      return this;
    }

    lambdaRuntime.createError(validationResult.error).send();

    return this;
  }

  /**
   * @param {String} schemaName
   * @param {*} valueObject
   * @param {Boolean} returnRaw
   * @returns {*}
   */
  validate(schemaName, valueObject, returnRaw = false) {
    let validationSchema = this.getSchema(schemaName);

    let result = Joi.validate(
      valueObject,
      validationSchema,
      {
        stripUnknown: true,
        convert: true,
        abortEarly: false,
      }
    );

    if (returnRaw) {
      return result;
    }

    if (result.error) {
      throw new ObjectValidationFailedException(schemaName, result.error);
    }

    return result.value;
  }

  /**
   * @todo: find a better way to inject libraries...
   *
   * @param {Function} cb
   * @returns {Object}
   */
  schemaFromValidationCb(cb) {
    return cb(Joi);
  }

  /**
   * @returns {Object[]}
   */
  get schemas() {
    return this._schemas;
  }

  /**
   * @returns {String[]}
   */
  get schemasNames() {
    return Object.keys(this._schemas);
  }

  /**
   * @param {String} schemaName
   * @param {Object} schema
   * @returns {Validation}
   */
  setSchemaRaw(schemaName, schema) {
    this._schemas[schemaName] = Validation.normalizeSchema(schema);

    return this;
  }

  /**
   * @param {String} schemaName
   * @param {Object} schema
   * @returns {Validation}
   */
  setSchema(schemaName, schema) {
    if (!schema.isJoi) {
      throw new InvalidJoiSchemaException(schemaName);
    }

    this._schemas[schemaName] = schema;

    return this;
  }

  /**
   * @param {String} schemaName
   * @param {Object} schema
   * @returns {Validation}
   */
  setGuessSchema(schemaName, schema) {
    if (typeof schema === 'function') {
      schema = this.schemaFromValidationCb(schema);
    }

    let setSchemaMethod = schema.isJoi ? 'setSchema' : 'setSchemaRaw';

    return this[setSchemaMethod](schemaName, schema);
  }

  /**
   * @param {String} schemaName
   * @returns {Boolean}
   */
  hasSchema(schemaName) {
    return typeof this._schemas[schemaName] !== 'undefined';
  }

  /**
   * @param {String} schemaName
   * @returns {Object}
   */
  getSchema(schemaName) {
    if (!this.hasSchema(schemaName)) {
      throw new ValidationSchemaNotFoundException(schemaName);
    }

    let schema = this._schemas[schemaName];

    // Let's assume it's an path while lazy loading in backend context
    if (typeof schema === 'string') {
      return this
        .setGuessSchema(schemaName, require(schema))
        .getSchema(schemaName);
    }

    return schema;
  }

  /**
   * Booting a certain service
   *
   * @param {Kernel} kernel
   * @param {Function} callback
   */
  boot(kernel, callback) {
    this._nonPartitionedModels = kernel.config.nonPartitionedModels || [];
    this._schemas = this._rawModelsToSchemas(kernel.config.models);

    let universalRequire = new Core.Generic.UniversalRequire();
    let remainingSchemas = kernel.config.validationSchemas.length;

    // Load custom schemas
    kernel.config.validationSchemas.forEach((schemaName) => {
      let relativeSchemaPath = path.join(
        Core.AWS.Lambda.Runtime.VALIDATION_SCHEMAS_DIR,
        `${schemaName}.js`
      );

      if (kernel.isBackend) {
        this._schemas[schemaName] = path.join(process.cwd(), relativeSchemaPath);

        remainingSchemas--;
      } else {
        let schemaUrl = path.join(path.sep, relativeSchemaPath);

        universalRequire.require(schemaUrl, (error, schemaObj) => {

          // @todo: abstract it somehow?
          if (error) {
            console.error(`Error while loading schema ${schemaName}: ${error}`);
          } else {
            this.setGuessSchema(schemaName, schemaObj);
          }

          remainingSchemas--;
        });
      }
    });

    let checkRemainingSchemas = (onFail) => {
      if (remainingSchemas <= 0) {
        callback();
      } else {
        setTimeout(() => {
          onFail(onFail);
        }, 50);
      }
    };

    checkRemainingSchemas(checkRemainingSchemas);
  }

  /**
   * @param {Array} rawModels
   * @returns {Object}
   * @private
   */
  _rawModelsToSchemas(rawModels) {
    let modelsSchema = {};

    for (let modelKey in rawModels) {
      if (!rawModels.hasOwnProperty(modelKey)) {
        continue;
      }

      let backendModels = rawModels[modelKey];

      for (let schemaName in backendModels) {
        if (!backendModels.hasOwnProperty(schemaName)) {
          continue;
        }

        let schema = backendModels[schemaName];

        // @todo: move this sh*t into DB somehow...
        schema.Id = schema.Id || 'timeUUID';

        modelsSchema[schemaName] = Validation.normalizeSchema(schema);

        if (!schema.AccountId && this._usePartitionField && this.isPartitionedModel(schemaName)) {
          modelsSchema[schemaName] = modelsSchema[schemaName].keys({
            AccountId: Joi.string().default('anonymous'),
          });
        }
      }
    }

    return modelsSchema;
  }

  /**
   * @returns {Boolean}
   * @private
   */
  get _usePartitionField() {
    return this._forcePartitionField || (this.kernel && this.kernel.accountMicroservice);
  }

  /**
   * @param {String} modelName
   * @returns {Boolean}
   */
  isPartitionedModel(modelName) {
    let modelIndex = this._nonPartitionedModels.indexOf(modelName);

    if (this.kernel) {
      return this.kernel.accountMicroservice && modelIndex === -1;
    } else {
      return modelIndex === -1;
    }
  }

  /**
   * @param {Object} rawSchema
   * @returns {Object}
   */
  static normalizeSchema(rawSchema) {
    return new ObjectToJoi(rawSchema).transform();
  }
}