Nedomas/databound-rails

View on GitHub
lib/databound/manager.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Databound
  class NotPermittedError < RuntimeError; STATUS = 405; end
  class ConfigError < RuntimeError; end

  class Manager
    def initialize(controller)
      @controller = controller

      @scope = Databound::Data.new(@controller, scope_js, model)
      @data = Databound::Data.new(@controller, data_js, model)

      @extra_where_scopes = JSON.parse(extra_where_scopes_js).map do |extra_scope|
        Databound::Data.new(@controller, extra_scope, model)
      end
    end

    def find_scoped_records(only_extra_scopes: false)
      records = or_query(@scope, *@extra_where_scopes)

      unless only_extra_scopes
        records = filter_by_params!(records)
        check_permit!(:read, records)
      end

      records
    end

    def create_from_data
      check_params!(:create)
      record = model.new(params.to_h)
      check_permit!(:create, record)

      record.save
      record
    end

    def update_from_data
      attributes = params.to_h
      id = attributes.delete(:id)

      check_params!(:update)
      record = model.find(id)
      check_permit!(:update, record)

      record.update(attributes)
      record
    end

    def destroy_from_data
      record = model.find(params.id)
      check_permit!(:destroy, record)
      record.destroy
    end

    def action_allowed?(method, record)
      permit_checks = @controller.databound_config.read(:permit)
      check = permit_checks[method]
      return true unless check

      @controller.instance_exec(params, record, &check)
    end

    private

    def or_query(*scopes)
      nodes = scopes.map do |scope|
        model.where(scope.to_h).where_values.reduce(:and)
      end

      model.where(nodes.reduce(:or)).tap do |q|
        q.bind_values = bound_values(scopes)
      end
    end

    def bound_values(scopes)
      scopes.flat_map do |scope|
        model.where(scope.to_h).bind_values
      end
    end

    def check_params!(action)
      @action = action
      return if columns == :all
      return if unpermitted_columns.empty?

      raise NotPermittedError, "Request includes unpermitted columns: #{unpermitted_columns.join(', ')}"
    end

    def check_permit!(method, record)
      return if action_allowed?(method, record)

      raise NotPermittedError, "Request for #{method} not permitted"
    end

    def permit_update_destroy_block
      @controller.class.permit_update_destroy
    end

    def unpermitted_columns
      params.to_h.keys - columns - allowed_action_columns
    end

    def params
      OpenStruct.new(@scope.to_h.merge(@data.to_h))
    end

    def allowed_action_columns
      @action == :update ? [:id] : []
    end

    def columns
      result = @controller.databound_config.read(:columns)

      case result
      when [:all]
        :all
      when [:table_columns]
        table_columns
      else
        Array(result)
      end
    end

    def table_columns
      if mongoid?
        model.fields.keys.map(&:to_sym)
      elsif activerecord?
        model.column_names.map(&:to_sym)
      else
        raise ConfigError, 'ORM not supported. Use ActiveRecord or Mongoid'
      end
    end

    def mongoid?
      defined?(Mongoid) and model.ancestors.include?(Mongoid::Document)
    end

    def activerecord?
      defined?(ActiveRecord) and model.ancestors.include?(ActiveRecord::Base)
    end

    def model
      raise ConfigError, 'No model specified' unless model_name

      model_name.to_s.camelize.constantize
    end

    def model_name
      @controller.databound_config.read(:model)
    end

    def scope_js
      @controller.params[:scope]
    end

    def data_js
      @controller.params[:data]
    end

    def extra_where_scopes_js
      @controller.params[:extra_where_scopes] || '[]'
    end

    def extra_scope_records
      @extra_where_scopes.flat_map(&:records)
    end

    def filter_by_params!(records)
      records & or_query(params, *@extra_where_scopes)
    end
  end
end