openjaf/cenit

View on GitHub
app/controllers/api/v3/api_controller.rb

Summary

Maintainability
F
2 wks
Test Coverage
module Api::V3
  class ApiController < ApplicationController

    include OAuth2AccountAuthorization
    include CorsCheck

    before_action :allow_origin_header
    before_action :authorize_account, except: [:new_user, :cors_check]
    before_action :save_request_data
    before_action :find_model, except: [:new_user, :cors_check]
    before_action :find_item, only: [:update, :show, :destroy, :digest]
    before_action :authorize_action, except: [:new_user, :cors_check]

    rescue_from Exception, with: :exception_handler

    respond_to :json

    def index
      setup_viewport
      items = select_items
      if (distinct = params[:format]&.match(/\Adistinct\((.+)\)\Z/))
        json = klass.collection.distinct(distinct[1], items.selector)
      else
        template_options.delete(:inspecting)
        if (model_ignore = klass.index_ignore_properties).present?
          template_options[:ignore] =
            if (ignore_option = template_options[:ignore])
              unless ignore_option.is_a?(Array)
                ignore_option = ignore_option.to_s.split(',').collect(&:strip)
              end
              ignore_option + model_ignore
            else
              model_ignore
            end
        end
        maximum_entries =
          if (account = Account.current)
            account.index_max_entries
          else
            Account::DEFAULT_INDEX_MAX_ENTRIES
          end
        template_options[:max_entries] =
          if (max_entries = template_options[:max_entries])
            max_entries = max_entries.to_i
            if max_entries.zero? || max_entries > maximum_entries
              maximum_entries
            else
              max_entries
            end
          else
            maximum_entries
          end
        items_data =
          if get_limit.zero?
            []
          else
            items.map do |item|
              Template.with(item) do |template|
                template.default_hash(template_options)
              end
            end
          end
        count = items.count
        json = {
          current_page: get_page,
          count: count,
          items: items_data,
          data_type: {
            (template_options[:raw_properties] ? :_id : :id) => klass.data_type.id.to_s
          }
        }
        if get_limit.positive?
          json[:total_pages] = (count * 1.0 / get_limit).ceil
        end
      end
      render json: json
    end

    def show
      item = @item
      if (json_path = request.headers['X-JSON-Path']) && json_path =~ /\A\$(.(^[.\[\]])*(\[[0-9]+\])?)+\Z/
        json_path = json_path.split('.')
        json_path.shift
        json_path.each do |access|
          index = if (match = access.match(/(.*)\[([0-9]+)\]\Z/))
            access = match[1]
            match[2].to_i
          end
          item = item.is_a?(Array) || item.is_a?(Hash) ? item[access] : item.send(access)
          item = item[index] if index
        end
      end
      setup_viewport
      render json: to_hash(item).to_json
    end

    def new
      parser_options = self.parser_options.dup
      if klass.is_a?(Class) && klass < FieldsInspection
        parser_options[:inspect_fields] = Account.current.nil? || !::User.super_access?
      end
      if klass.is_a?(Class) && klass < Setup::AsynchronousPersistence::Model
        execution = Setup::AsynchronousPersistence.process(
          parser_options: parser_options,
          data_type_id: klass.data_type.id,
          access_scope: @oauth_scope&.to_s,
          data: request_data
        )
        render json: execution.to_hash(include_id: true, include_blanks: false), status: :accepted
      else
        parser = Parser.new(klass.data_type)
        parser_options[:create_callback] = -> model {
          fail Abort unless authorize_action(action: :create, klass: model)
        }
        parser_options[:update_callback] = -> record {
          fail Abort unless authorize_action(action: :update, item: record)
        }
        record = parser.create_from(request_data, parser_options)
        if record.errors.blank?
          if setup_viewport(:headers)
            render json: to_hash(record)
          else
            render body: nil
          end
        else
          render json: klass.pretty_errors(record), status: :unprocessable_entity
        end
      end
    rescue Abort
      # Aborted!
    end

    def update
      parser_options = self.parser_options.dup
      parser_options[:add_only] = true
      async = klass.is_a?(Class) && klass < Setup::AsynchronousPersistence::Model
      if async
        execution = Setup::AsynchronousPersistence.process(
          parser_options: parser_options,
          data_type_id: klass.data_type.id,
          record_id: @item.id,
          access_scope: @oauth_scope&.to_s,
          data: request_data,
          inspect_fields: Account.current.nil? || !::User.super_access?
        )
        render json: execution.to_hash(include_id: true, include_blanks: false), status: :accepted
      else
        @item.fill_from(request_data, parser_options)
        save_options = {}
        if @item.class.is_a?(Class) && @item.class < FieldsInspection
          save_options[:inspect_fields] = Account.current.nil? || !::User.super_access?
        end
        if Cenit::Utility.save(@item, save_options)
          if setup_viewport(:headers)
            render json: to_hash(@item)
          else
            render body: nil
          end
        else
          render json: klass.pretty_errors(@item), status: :unprocessable_entity
        end
      end
    end

    def destroy
      if @item.destroy
        render body: nil
      else
        render json: klass.pretty_errors(@item), status: :unprocessable_entity
      end
    end

    USER_MODEL_FIELDS = %w(name email password)
    USER_API_FIELDS = USER_MODEL_FIELDS + %w(token code)

    def new_user
      data = (JSON.parse(request_data) rescue {}).keep_if { |key, _| USER_API_FIELDS.include?(key) }
      data = data.with_indifferent_access
      data.reverse_merge!(email: params[:email], password: pwd = params[:password])
      data.reject! { |_, value| value.nil? }
      status = :not_acceptable
      response =
        if (token = data[:token] || params[:token])
          if (captcha_token = CaptchaToken.where(token: token).first)
            if (code = data[:code] || params[:code])
              if code == captcha_token.code
                token_data = captcha_token.data || {}
                if !data.key?(:email) || data[:email] == token_data[:email]
                  data.merge!(captcha_token.data || {}) { |_, left, right| left || right }
                  captcha_token.destroy
                  _, status, response = create_user_with(data)
                  response
                else #email mismatch
                  { email: ['does not match the one previously requested'] }
                end
              else #invalid code
                { code: ['is not valid'] }
              end
            else #code missing
              { code: ['is missing'] }
            end
          else #invalid token
            { token: ['is not valid'] }
          end
        elsif data[:email]
          data[:password] = Devise.friendly_token unless data[:password]
          if (user = User.new(data)).valid?(context: :create)
            if (captcha_token = CaptchaToken.create(email: data[:email], data: data)).errors.blank?
              status = :ok
              { token: captcha_token.token }
            else
              captcha_token.errors.to_json
            end
          else
            user.errors.to_json
          end
        else #bad request
          status = :bad_request
          { token: ['is missing'], email: ['is missing'] }
        end
      render json: response, status: status
    end

    def digest
      request.body.rewind
      options =
        begin
          JSON.parse(request.headers['X-Digest-Options'])
        rescue
          nil
        end
      options = {} unless options.is_a?(Hash)
      path = (params[:_digest_path] || '').split('/').map(&:presence).compact.join('_').presence
      path = path ? "digest_#{path}" : :digest
      no_logic = false
      if @item.respond_to?(method = "#{request.method.to_s.downcase}_#{path}") || @item.respond_to?(method = path)
        render @item.send(method, request, options)
      elsif @item.respond_to?(method = "handle_#{request.method.to_s.downcase}_#{path}")
        @item.send(method, self)
      elsif @item.is_a?(Setup::CenitDataType)
        model = @item.build_in.model
        if model.respond_to?(method = "#{request.method.to_s.downcase}_#{path}") || model.respond_to?(method = path)
          render model.send(method, request, options)
        elsif model.respond_to?(method = "handle_#{request.method.to_s.downcase}_#{path}")
          @item.send(method, self)
        else
          no_logic = true
        end
      else
        no_logic = true
      end
      if no_logic
        render json: {
          error: "No processable logic defined by #{@item.orm_model.data_type.custom_title}"
        }, status: :not_acceptable
      end
    end

    attr_reader :model

    def setup_request(options = {})
      if (@klass = options[:klass])
        @ns_name = @klass.data_type.namespace
      else
        @ns_name = nil
      end
      @ns_slug = options[:namespace] || params[:__ns_]
      @model = options[:model] || params[:__model_]
      @_id = options[:id] || params[:__id_]
      @format = options[:format] || params[:format]
      @path = options[:path] || "#{params[:path]}.#{params[:format]}" if params[:path] && params[:format]
      query_selector(options[:selector])
    end

    def find_item
      if (id = @_id) == 'me' && klass == User
        id = User.current_id
      elsif id == 'current' && klass == Account
        id = Account.current_id
      end
      if (@item = accessible_records.where(id: id).first)
        @item
      else
        render json: { status: 'item not found' }, status: :not_found
        false
      end
    end

    PARSER_OPTIONS = %w(add_only primary_field ignore reset update skip_refs_binding add_new).collect(&:to_sym)

    def parser_options
      @parser_options ||=
        begin
          unless (opts = Cenit::Utility.json_value_of(request.headers['X-Parser-Options'])).is_a?(Hash)
            opts = {}
          end
          PARSER_OPTIONS.each do |opt|
            next unless params.key?(opt)
            opts[opt] = Cenit::Utility.json_value_of(params[opt])
          end
          %w(primary_field primary_fields ignore reset update).each do |option|
            unless (value = opts.delete(option)).is_a?(Array)
              value = value.to_s.split(',').collect(&:strip)
            end
            opts[option] = value
          end
          opts
        end
    end

    def template_options
      @template_options ||=
        begin
          unless (opts = Cenit::Utility.json_value_of(request.headers['X-Template-Options'])).is_a?(Hash)
            opts = {}
          end
          opts = opts.with_indifferent_access
          if query_selector.present? && klass
            %w(only ignore embedding).each do |option|
              if query_selector.key?(option) && !klass.property?(option)
                unless (value = query_selector.delete(option)).is_a?(Array)
                  value = value.to_s.split(',').collect(&:strip)
                end
                opts[option] = value
              end
            end
          end
          if (fields_option = query_selector.delete(:fields)) || !opts.key?(:only)
            fields_option =
              case fields_option
                when Array
                  fields_option
                when Hash
                  fields_option.collect { |field, presence| presence.to_b ? field : nil }.select(&:presence)
                else
                  fields_option.to_s.split(',').collect(&:strip)
              end
            opts[:only] = fields_option
          end
          opts
        end
    end

    def setup_viewport(source = nil)
      unless source == :headers || template_options.key?(:viewport) || request_data.blank?
        template_options[:viewport] = request_data
      end
      unless template_options[:viewport] || template_options.key?(:include_id)
        template_options[:include_id] = true
      end
      template_options.key?(:viewport)
    end

    def query_options
      @query_options ||=
        if (opts = Cenit::Utility.json_value_of(request.headers['X-Query-Options'])).is_a?(Hash)
          opts
        else
          {}
        end.with_indifferent_access
    end

    def query_selector(selector = nil)
      parse_query_params =
        if selector
          @criteria = nil
        else
          selector = request.headers['X-Query-Selector']
          true
        end
      @criteria ||=
        begin
          unless (selector = Cenit::Utility.json_value_of(selector)).is_a?(Hash)
            selector = {}
          end
          selector = selector.with_indifferent_access
          if parse_query_params
            selector.merge!(
              params.permit!.reject do |key, _|
                %w(controller action __ns_ __model_ __id_ format api _digest_path).include?(key)
              end
            )
          end
          %w(page limit).each do |key|
            next unless selector.key?(key) && klass && !klass.property?(key)
            query_options[key] = selector.delete(key)
          end
          selector
        end
    end

    def create_user_with(data)
      status = :not_acceptable
      data[:password] ||= Devise.friendly_token
      data.reject! { |key, _| USER_MODEL_FIELDS.exclude?(key) }
      current_account = Account.current
      begin
        Account.current = nil
        (user = ::User.new(data)).save
      rescue
        user #TODO Handle sending confirmation email error
      ensure
        Account.current = current_account
      end
      response =
        if user.errors.blank?
          status = :ok
          { id: user.id.to_s, number: user.number, token: user.authentication_token }
        else
          user.errors.to_json
        end
      [user, status, response]
    end

    def get_limit
      @limit ||=
        begin
          limit_option = query_options.delete(:limit)
          limit = (query_selector.delete(:limit) || limit_option || Kaminari.config.default_per_page).to_i
          if limit.negative?
            Kaminari.config.default_per_page
          else
            [Kaminari.config.default_per_page, limit].min
          end
        end
    end

    def get_page
      @page ||=
        if (page = query_options.delete(:page))
          page.to_i
        else
          1
        end
    end

    def select_items
      asc = true
      if (order = query_selector.delete(:order))
        order.strip!
        asc = !order.match(/^-.*/)
      end

      limit = get_limit
      page = get_page
      skip = page < 1 ? 0 : (page - 1) * limit

      items = accessible_records.limit(limit).skip(skip).and(query_selector)

      if (sort = query_options[:sort])
        sort.each do |field, sort_option|
          items =
            case sort_option
              when 1
                items.asc(field)
              when -1
                items.desc(field)
              else
                items
            end
        end
      end
      if order
        if asc
          items.ascending(*order.split(','))
        else
          items.descending(*order.slice(1..-1).split(','))
        end
      else
        items
      end
    end

    def to_hash(item)
      Template.with(item) { |template| template.to_hash(template_options) }
    end

    def authorize_action(options = {})
      action = options[:action] || @_action_name
      success = true
      if klass
        action_symbol =
          case action
            when 'index', 'show'
              :read
            when 'new'
              :create
            when 'destroy'
              :delete
            else
              action.to_sym
          end
        if @ability.can?(action_symbol, options[:item] || options[:klass] || @item || klass) &&
          (@oauth_scope.nil? || @oauth_scope.can?(action_symbol, options[:klass] || klass))
          @access_token.hit if @access_token
        else
          success = false
          unless options[:skip_response]
            error_description = 'The requested action is out of the access token scope'
            response.headers['WWW-Authenticate'] = %(Bearer realm="example",error="insufficient_scope",error_description=#{error_description})
            render json: { error: 'insufficient_scope', error_description: error_description }, status: :forbidden
          end
        end
      else
        success = false
        unless options[:skip_response]
          if Account.current
            render json: { error: 'no model found' }, status: :not_found
          else
            error_description = 'The requested action is out of the access token scope'
            response.headers['WWW-Authenticate'] = %(Bearer realm="example",error="insufficient_scope",error_description=#{error_description})
            render json: { error: 'insufficient_scope', error_description: error_description }, status: :forbidden
          end
        end
      end
      success
    end

    protected

    def authorized_action?
      authorize_action(skip_response: true)
    end

    def exception_handler(exception)
      responder = Cenit::Responder.new(@request_id, exception)
      render json: responder, root: false, status: responder.code
      false
    end

    def find_model
      if klass
        true
      else
        render json: { status: 'model not found' }, status: :not_found
        false
      end
    end

    def get_data_type_by_slug(slug)
      if slug
        @data_types[slug] ||=
          begin
            build_in = nil
            if @ns_slug == 'setup' || @ns_slug == 'cenit'
              build_in_name =
                if slug == 'trace'
                  Mongoid::Tracer::Trace.to_s
                else
                  "#{@ns_slug.camelize}::#{slug.camelize}"
                end
              build_in =
                Setup::BuildInDataType[build_in_name] ||
                  Setup::BuildInDataType[slug.camelize] ||
                  Setup::BuildInFileType[build_in_name]
            end
            build_in ||
              begin
                if @ns_name.nil?
                  ns = Setup::Namespace.where(slug: @ns_slug).first
                  @ns_name = ns&.name || ''
                end
                if @ns_name
                  Setup::DataType.where(namespace: @ns_name, slug: slug).first ||
                    Setup::DataType.where(namespace: @ns_name, slug: slug.singularize).first ||
                    Setup::DataType.where(namespace: @ns_name.camelize, name: slug.camelize).first
                else
                  nil
                end
              end
          end
      else
        nil
      end
    end

    def get_data_type(root)
      get_data_type_by_slug(root) if root
    end

    def get_model(root)
      if (data_type = get_data_type(root))
        data_type.records_model
      else
        nil
      end
    end

    def klass
      @klass ||= get_model(model)
    end

    def accessible_records
      (@ability && klass.accessible_by(@ability, :read)) || klass.all
    end

    def save_request_data
      @data_types ||= {}
      @request_id = request.uuid
      @request_data = request.body.read
      setup_request
    end

    private

    attr_reader :request_data

    class Parser
      include Setup::DataTypeParser

      def initialize(data_type)
        @data_type = data_type
      end

      def parser_data_type
        @data_type
      end

      def method_missing(symbol, *args, &block)
        parser_data_type.send(symbol, *args, &block)
      end
    end

    class Template
      class << self
        include Edi::Formatter

        def with(record)
          Thread.current[SELF_RECORD_KEY] = record
          yield self
        ensure
          Thread.current[SELF_RECORD_KEY] = nil
        end

        def self_record
          Thread.current[SELF_RECORD_KEY]
        end

      end

      SELF_RECORD_KEY = "[cenit]#{self}.self_record"
    end

    class Abort < Exception; end
  end
end

module Cenit
  App.module_eval do

    def post_digest_config(request, _options = {})
      self.configuration = request.body.read
      if save
        {
          body: nil
        }
      else
        {
          json: self.class.pretty_errors(self),
          status: :unprocessable_entity
        }
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  BuildInApp.module_eval do

    def get_digest_reinstall(_request, _options = {})
      if User.current_super_admin?
        execution = ::Setup::BuildInAppReinstall.process(
          build_in_app_id: id,
          task_description: "Re-installing build-in app #{app_module_name}"
        )
        if execution.is_a?(Setup::SystemNotification)
          fail execution.message
        end
        {
          json: execution.to_hash(include_id: true, include_blanks: false)
        }
      else
        {
          json: { '$': 'Not authorized' },
          status: :unauthorized
        }
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  ApplicationId.class_eval do

    def get_digest_switch_trust(_request, options = {})
      self.trusted = !trusted
      if save
        {
          json: to_hash(options)
        }
      else
        {
          json: self.class.pretty_errors(self),
          status: :unprocessable_entity
        }
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  ActiveTenant.class_eval do
    class << self

      def get_digest_clean(_request, _options = {})
        clean_all
        {
          body: nil
        }
      rescue
        {
          json: { '$': [$!.message] },
          status: :unprocessable_entity
        }
      end

      def get_digest_list(_request, _options = {})
        hash = to_hash
        active_tenants = ::Account.where(:id.in => hash.keys).to_a.map do |tenant|
          {
            tenant: {
              _reference: true,
              id: tenant.id.to_s,
              name: tenant.name
            },
            tasks: hash[tenant.id.to_s]
          }
        end
        {
          json: active_tenants
        }
      rescue
        {
          json: { '$': [$!.message] },
          status: :unprocessable_entity
        }
      end
    end
  end

  OauthAccessGrant.class_eval do

    def get_digest_tokens(_request, _options = {})
      {
        json: tokens.map do |token|
          {
            id: token.id.to_s,
            token: token.token,
            expires_at: token.expires_at,
            note: token.data && token.data[:note]
          }
        end
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def post_digest_token(request, _options = {})
      error_field = '$'
      body = request.body.read
      payload =
        if body.blank?
          {}
        else
          JSON.parse(body)
        end.with_indifferent_access
      token_span = payload[:token_span]
      note = payload[:note]
      error_field = 'token_span'
      fail 'Expected to be a number' unless token_span && token_span.is_a?(Numeric)
      fail 'Expected to be in the future' unless token_span >= 0
      error_field = 'note'
      fail 'Expected to be a string' unless note && note.is_a?(String)
      fail 'Is too long' if note.length > 255
      {
        json: Cenit::OauthAccessToken.for(
          application_id,
          scope,
          ::User.current,
          token_span: token_span,
          note: note
        )
      }
    rescue
      {
        json: { error_field => [$!.message] },
        status: :unprocessable_entity
      }
    end

    def delete_digest_token(request, _options = {})
      token = OauthAccessToken.where(id: request.params[:id], token: request.params[:token]).first
      if token
        if token.destroy
          {
            body: nil
          }
        else
          {
            json: OauthAccessToken.pretty_errors(token),
            status: :unprocessable_entity
          }
        end
      else
        {
          body: nil, status: :not_found
        }
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end
end

module Setup
  DataType.class_eval do

    def handle_get_digest(controller)
      if (id = controller.request.headers['X-Record-Id'])
        controller.setup_request(namespace: ns_slug, klass: records_model, id: id)
        controller.show if (item = controller.find_item) && controller.authorize_action(
          action: :read,
          item: item,
          klass: records_model
        )
      else
        controller.setup_request(namespace: ns_slug, klass: records_model)
        controller.index if controller.authorize_action(
          action: :read,
          klass: records_model
        )
      end
    end

    def handle_get_digest_search(controller)
      request_selector = controller.query_selector
      query = request_selector.delete('query')
      search_selector = records_model.search_selector(query)
      unless request_selector.empty?
        search_selector = { '$and' => [request_selector, search_selector] }
      end
      controller.setup_request(
        namespace: ns_slug,
        klass: records_model,
        selector: search_selector
      )
      controller.index if controller.authorize_action(
        action: :read,
        klass: records_model
      )
    end

    def handle_post_digest(controller)
      controller.setup_request(namespace: ns_slug, klass: records_model)
      controller.new
    end

    def handle_delete_digest(controller)
      query = where(controller.query_selector)
      response =
        if query.count == 1
          item = query.first
          if controller.authorize_action(action: :delete, item: item, klass: records_model)
            if item.destroy
              { body: nil }
            else
              {
                json: records_model.pretty_errors(item),
                status: :unprocessable_entity
              }
            end
          end
        elsif controller.authorize_action(action: :delete, klass: records_model)
          execution = Deletion.process(
            data_type_id: id,
            selector: controller.query_selector.to_json,
            task_description: "Deleting #{name.to_title.pluralize}"
          )
          {
            json: controller.to_hash(execution),
            status: :accepted
          }
        end
      controller.render response if response
    end

    def digest_schema(request, options = {})
      data =
        if request.get?
          merged_schema(options)
        else
          merge_schema(JSON.parse(request.body.read), options)
        end
      {
        json: data
      }
    rescue Exception => ex
      {
        json: { error: ex.message },
        status: :bad_request
      }
    end

    def get_digest_origins(_request, _options = {})
      model = records_model
      origins =
        if (model.is_a?(Class) && model < CrossOrigin::Document) || model == Collection
          origins = model.origins
                      .map(&:to_sym)
                      .select { |origin| Crossing.authorized_crossing_origins.include?(origin) }
                      .map { |a| [a, a] }.to_h
          unless User.current_cross_shared?
            origins.delete(:shared)
          end
          origins.keys
        else
          [:default]
        end
      {
        body: origins.map(&:to_s)
      }
    rescue
      {
        json: { error: $!.message },
        status: :bad_request
      }
    end

    def post_digest_cross(request, _options = {})
      fail 'Unable to cross' unless model.is_a?(Class) && (
        model < CrossOrigin::Document ||
        model == Collection
      )
      options = JSON.parse(request.body.read)
      execution = Setup::Crossing.process(
        data_type_id: id,
        selector: options['selector'].to_json,
        origin: options['origin'],
        task_description: "Crossing #{name.to_title.pluralize}"
        #TODO Cross dependencies option
        )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def post_digest_pull_import(request, options = {})
      readable =
        if request.content_type.downcase == 'multipart/form-data'
          request.params[:data] || request.params[:file] || fail('Missing data (or file) part')
        else
          request.body
        end
      readable.rewind
      data =
        begin
          JSON.parse(readable.read)
        rescue
          fail 'Invalid JSON data'
        end
      data = [data] if data.is_a?(Hash)
      model = records_model
      if model == Setup::Collection
        if data.length == 1
          data = data[0]
        else
          fail 'Array data is not allowed for pulling collections'
        end
      else
        collecting_property = Setup::CrossSharedCollection::COLLECTING_PROPERTIES.detect { |name| Setup::CrossSharedCollection.reflect_on_association(name).klass >= model }
        data = { collecting_property => data }.with_indifferent_access
      end
      execution = Setup::PullImport.process(
        data: data.to_json,
        discard_collection: model != Setup::Collection,
        task_description: options['task_description']
      )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { error: $!.message },
        status: :bad_request
      }
    end
  end

  [FileDataType, JsonDataType].each do |k|
    k.class_eval do

      def get_digest_config(_request, _options = {})
        if (config = self.config).new_record?
          config = {}
        else
          config = config.to_hash
        end
        {
          json: config
        }
      rescue
        {
          json: { '$': [$!.message] },
          status: :unprocessable_entity
        }
      end

      def post_digest_config(request, _options = {})
        hash = JSON.parse(request.body.read)
        config.slug = hash['slug']
        config.trace_on_default = hash['trace_on_default']
        config.save!
        {
          json: config.to_hash(include_id: true)
        }
      rescue
        {
          json: { '$': [$!.message] },
          status: :unprocessable_entity
        }
      end
    end
  end

  [PullImport, SharedCollectionPull, ApiPull].each do |pull_model|
    pull_model.class_eval do

      def post_digest_pull(request, _options = {})
        options = JSON.parse(request.body.read)
        message[:install] = options['install'].to_b if ask_for_install?
        unless (pull_parameters = options['pull_parameters']).is_a?(Hash)
          pull_parameters = {}
        end
        message[:pull_parameters] = pull_parameters
        execution = self.retry
        if execution.is_a?(Setup::SystemNotification)
          fail execution.message
        end
        {
          json: execution.to_hash(include_id: true, include_blanks: false),
          status: :accepted
        }
      rescue
        {
          json: { error: $!.message },
          status: :bad_request
        }
      end
    end
  end

  FileDataType.class_eval do

    def post_digest_upload(request, options = {})
      readable =
        if request.content_type.downcase == 'multipart/form-data'
          request.params[:data] || request.params[:file] || fail('Missing data (or file) part')
        else
          request.body
        end
      readable.rewind
      file = create_from(readable, options)
      if file.errors.present?
        {
          json: records_model.pretty_errors(file),
          status: :unprocessable_entity
        }
      else
        {
          json: Api::V3::ApiController::Template.with(file) do |template|
            template.to_hash(only: %w(id filename contentType length md5 public_url))
          end
        }
      end
    rescue Exception => ex
      {
        json: { error: ex.message },
        status: :bad_request
      }
    end

    def handle_get_digest_download(controller)
      file = where(controller.query_selector).first
      if file
        controller.send_data(file.data, filename: file.filename, type: file.contentType)
      else
        controller.render body: nil, status: :not_found
      end
    rescue
      controller.render json: { error: $!.message }, status: :bad_request
    end
  end

  Flow.class_eval do

    def post_digest(request, _options = {})
      begin
        message = JSON.parse(request.body.read)
        fail unless message.is_a?(Hash)
      rescue
        message = {}
      end
      if (selector = message['selector'])
        message['selector'] = selector.to_json
      end
      begin
        execution = process(message.with_indifferent_access)
        if execution.is_a?(Setup::SystemNotification)
          fail execution.message
        end
        execution.reload
        {
          json: execution.to_hash(include_id: true, include_blanks: false)
        }
      rescue
        {
          json: { error: $!.message },
          status: :bad_request
        }
      end
    end
  end

  Algorithm.class_eval do

    def post_digest(request, _options = {})
      hash = input = JSON.parse(request.body.read)
      if input.is_a?(Array)
        hash = parameters.map { |p, index| [p.name, input[index]] }
      else
        input = parameters.map { |p| hash[p.name] }
      end
      Mongoff::Validator.validate_instance(
        hash,
        schema: configuration_schema,
        data_type: self.class.data_type
      )
      execution = run_asynchronous(input)
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      execution.reload
      {
        json: execution.to_hash(include_id: true, include_blanks: false)
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  Template.class_eval do

    def post_digest(request, _options = {})
      options = JSON.parse(request.body.read)
      execution = Setup::Translation.process(
        translator_id: id,
        data_type_id: (
          options['source_data_type_id'] ||
            options['data_type_id'] ||
              ((data_type = options['data_type']) && data_type['id']) ||
                source_data_type_id
        ),
        selector: options['selector'].to_json,
        skip_notification_level: true,
        #options: @form_object.options TODO Template options
      )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  ParserTransformation.class_eval do

    def post_digest(request, options = {})
      readable =
        if request.content_type.downcase == 'multipart/form-data'
          request.params[:data] || request.params[:file] || fail('Missing data (or file) part')
        else
          request.body
        end
      readable.rewind
      msg = {
        translator_id: id,
        data_type_id: (
          options['target_data_type_id'] ||
            options['data_type_id'] ||
              ((data_type = options['data_type']) && data_type['id']) ||
                target_data_type_id
        ),
        #decompress_content: decompress, TODO Parser options
        data: BSON::Binary.new(readable.read),
        #options: @form_object.options
      }
      execution = Setup::DataImport.process(msg)
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  UpdaterTransformation.class_eval do

    def post_digest(request, _options = {})
      options = JSON.parse(request.body.read)
      execution = Setup::Translation.process(
        translator_id: id,
        data_type_id: (
          options['target_data_type_id'] ||
            options['data_type_id'] ||
              ((data_type = options['data_type']) && data_type['id']) ||
                target_data_type_id
        ),
        selector: options['selector'].to_json,
        skip_notification_level: true,
        #options: @form_object.options TODO Updater options
        )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  ConverterTransformation.class_eval do

    def post_digest(request, _options = {})
      options = JSON.parse(request.body.read)
      execution = Setup::Translation.process(
        translator_id: id,
        data_type_id: (
        options['target_data_type_id'] ||
          options['data_type_id'] ||
          ((data_type = options['data_type']) && data_type['id']) ||
          target_data_type_id
        ),
        selector: options['selector'].to_json,
        skip_notification_level: true,
      #options: @form_object.options TODO Convert options
        )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  Collection.class_eval do

    def post_digest_share(request, _options = {})
      # TODO Validate pull parameters
      execution = Setup::CollectionSharing.process(
        collection_id: id,
        data: request.body.read,
        task_description: "Sharing collection #{name}"
      )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def delete_digest_shred(_request, _options = {})
      execution = Setup::CollectionShredding.process(
        collection_id: id,
        task_description: "Shredding collection #{name}"
      )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def post_digest_push(request, _options = {})
      options = JSON.parse(request.body.read)
      execution = Setup::Push.process(
        source_collection_id: id,
        shared_collection_id: (
          options['shared_collection_id'] ||
            ((shared_collection = options['shared_collection']) && shared_collection['id'])
        ),
        task_description: "Pushing #{name}"
      )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  CrossSharedCollection.class_eval do

    def get_digest_reinstall(_request, _options = {})
      execution = SharedCollectionReinstall.process(
        shared_collection_id: id,
        task_description: "Re-installing #{name}"
      )
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false)
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  [CrossSharedCollection, ApiSpec].each do |model|
    model.class_eval do

      def post_digest_pull(request, _options)
        data = JSON.parse(request.body.read)
        execution = pull(
          pull_parameters: data['pull_parameters'] || {}
        )
        if execution.is_a?(Setup::SystemNotification)
          fail execution.message
        end
        {
          json: execution.to_hash(include_id: true, include_blanks: false),
          status: :accepted
        }
      rescue
        {
          json: { '$': [$!.message] },
          status: :unprocessable_entity
        }
      end
    end
  end

  Task.class_eval do

    def get_digest_retry(_request, _options = {})
      if (execution = self.retry)
        if execution.is_a?(Setup::SystemNotification)
          fail execution.message
        end
        {
          json: execution.to_hash(include_id: true, include_blanks: false)
        }
      else
        fail "Can't retry at this moment"
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def post_digest_schedule(request, _options = {})
      sch_data = request.body.read.strip
      sch_data = sch_data.empty? ? {} : JSON.parse(sch_data)
      scheduler =
        if sch_data.empty?
          nil
        else
          Setup::Scheduler.create_from_json!(sch_data)
        end
      execution = schedule(scheduler, :exception)
      unless execution
        fail "Can't #{scheduler ? 're-' : 'un'}schedule right now, the task is #{status}"
      end
      if execution.is_a?(Setup::SystemNotification)
        fail execution.message
      end
      {
        json: execution.to_hash(include_id: true, include_blanks: false),
        status: :accepted
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  Application.class_eval do

    def get_digest_access(_request, options = {})
      access_grant = ::Cenit::OauthAccessGrant.where(
        application_id_id: application_id_id
      ).first
      if access_grant
        {
          json: access_grant.to_hash(options)
        }
      else
        {
          body: nil,
          status: :not_found
        }
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def post_digest_access(request, options = {})
      scope = ::Cenit::OauthScope.new(JSON.parse(request.body.read)['scope'])
      fail 'Is not valid' unless scope.valid?
      access_grant = ::Cenit::OauthAccessGrant.where(
        application_id_id: application_id_id
      ).first
      if access_grant
        access_grant.scope = scope.to_s
      else
        access_grant = ::Cenit::OauthAccessGrant.new(
          application_id_id: application_id_id,
          scope: scope.to_s
        )
      end
      access_grant.save!
      {
        json: access_grant.to_hash(options)
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def get_digest_registration(_request, _options = {})
      {
        json: {
          slug: application_id.slug.presence,
          oauth_name: application_id.oauth_name.presence
        }
      }
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end

    def post_digest_registration(request, options = {})
      data = JSON.parse(request.body.read).with_indifferent_access
      app_id = application_id
      if app_id.regist_with(data).valid? && app_id.save
        {
          json: app_id.to_hash(options)
        }
      else
        {
          json: ::Cenit::ApplicationId.pretty_errors(app_id),
          status: :unprocessable_entity
        }
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end

  EmailNotification.class_eval do

    def self.get_digest_email_data_type(_request, _options = {})
      if (data_type = Setup::Configuration.singleton_record.email_data_type)
        {
          json: {
            id: data_type.id.to_s,
            namespace: data_type.namespace,
            name: data_type.name
          }
        }
      else
        {
          body: nil
        }
      end
    rescue
      {
        json: { '$': [$!.message] },
        status: :unprocessable_entity
      }
    end
  end
end

require 'mongoff/grid_fs/file'

module Mongoff
  module GridFs
    File.class_eval do

      def post_digest(request, options = {})
        fill_from(options)
        self.data = request.body
        save
        {
          json: Api::V3::ApiController::Template.with(self) { |template| template.to_hash }
        }
      rescue Exception => ex
        {
          json: { error: ex.message },
          status: :bad_request
        }
      end

      def handle_get_digest(controller)
        controller.send_data(data, filename: filename, type: contentType)
      end
    end
  end
end

{
  get: {
    cancel: Cancelable,
    switch: Switchable
  }
}.each do |method, actions|
  actions.each do |action, mod|
    mod.module_eval <<-RUBY
      def #{method}_digest_#{action}(_request, options = {})
        #{action}
        {
          json: to_hash(options)
        }
      rescue
        {
          json: { error: $!.message },
          status: :bad_request
        }
      end
    
      ClassMethods.module_eval do
        def #{method}_digest_#{action}(_request, options = {})
          #{action}_all(where(options['selector'] || {}))
          {
            body: nil
          }
        rescue
          {
            json: { error: $!.message },
            status: :bad_request
          }
        end
      end
    RUBY
  end
end

::Account.class_eval do

  def delete_digest_shred(_request, _options = {})
    clean_up
    {
      body: nil
    }
  rescue
    {
      json: { '$': [$!.message] },
      status: :unprocessable_entity
    }
  end
end

::Script.class_eval do

  def post_digest(request, _options = {})
    hash = input = JSON.parse(request.body.read)
    if input.is_a?(Array)
      hash = parameters.map { |p, index| [p.name, input[index]] }
    else
      input = parameters.map { |p| hash[p.name] }
    end
    # TODO Validates input hash
    execution = ::ScriptExecution.process(
      script_id: id,
      input: input,
      skip_notification_level: true
    )
    if execution.is_a?(Setup::SystemNotification)
      fail execution.message
    end
    {
      json: execution.to_hash(include_id: true, include_blanks: false),
      status: :accepted
    }
  rescue
    {
      json: { '$': [$!.message] },
      status: :unprocessable_entity
    }
  end
end

::User.class_eval do
  def get_digest_switch_sudo(_request, options = {})
    if has_role?(:super_admin)
      if update(super_admin_enabled: !super_admin_enabled)
        {
          json: to_hash(options)
        }
      else
        {
          json: self.class.pretty_errors(self),
          status: :unprocessable_entity
        }
      end
    else
      {
        json: { '$': 'Not super user' },
        status: :unauthorized
      }
    end
  rescue
    {
      json: { '$': [$!.message] },
      status: :unprocessable_entity
    }
  end
end