arbaaz
7/7/2018 - 7:19 AM

Simple Lightweight module to create no action controllers

Simple Lightweight module to create no action controllers

require 'active_support'
module CRUDActions
  extend ActiveSupport::Concern
  
  included do
    before_action :set_resource, only: [:show, :update, :destroy]
  end
  
  # Returns count of records matching the scope
  def count
    render :status => :ok, :json => { resource_key => {:count => build_scope.count} }
  end

  # Returns count of records matching the scope for each of the unique group_by fields
  def group_count
    render :status => :ok, :json => build_scope.count
  end

  # GET /resources
  # Generic Index action for any resource
  # @param { query_options: { table_namespaced_field_name: value } }
  # @param { scopes: { scope_name: value } }
  # @param { joins: [ association_name1, association_name2 ] }
  # @param { select: [ table_namespaced_field_name1, table_namespaced_field_name2 ] }
  # @param { page: page_number }
  # @param { per: per_page }
  # @result [{ resource_root: { field1: value, field2: value } }]
  def index
    @resources = build_scope

    render json: @resources.to_json(build_json_parameters)
  end

  # GET /resources/1
  def show
    render json: @resource.to_json(build_json_parameters)
  end

  # POST /resources
  def create
    @resource = api_resource.new(create_params)
    before_save
    if @resource.save
      render json: @resource.to_json(build_json_parameters), status: :created
    else
      render json: @resource.to_json(build_json_parameters), status: :unprocessable_entity
    end
  end

  # PATCH/PUT /resources/1
  def update
    before_save
    if @resource.update(update_params)
      render json: @resource.to_json(build_json_parameters)
    else
      render json: @resource.to_json(build_json_parameters), status: :unprocessable_entity
    end
  end
  
  # Updates all records matching the scope with the update_attributes
  # Use with caution
  def update_all
    @entities = build_scope
    @entities.update_all(create_params[:update_attributes])
    render json: @entities.to_json(build_json_parameters)
  end

  # DELETE /resources/1
  def destroy
    @resource.destroy
  end
  
  # Destroys every record in the table
  # Use with caution
  def destroy_all
    table_name = api_resource.table_name
    delete_sql = "DELETE FROM #{table_name};"

    response = api_resource.new
    begin
      api_resource.connection.execute delete_sql
      render :status => :ok, :json => response.to_json
    rescue ActiveRecord::InvalidForeignKey => invalidForeignKey
      response.errors.add :id, invalidForeignKey.original_exception.message
      render :status => :unprocessable_entity, :json => response.to_json
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_resource
      @resource = api_resource.find(params[:id])
    end

    # Only allow trusted parameters while creating resource
    def create_params
      params.require(resource_key.to_sym).permit(permitted_params)
    end
    
    # Only allow trusted parameters while updating resource
    def update_params
      params.require(resource_key.to_sym).permit(permitted_params)
    end
    
    # Returns the primary_key for the api_resource
    # Can be overridden for specific resources
    # E.g. _id for Mongoid Models
    def primary_key
      :id
    end

    # Returns resource_key for the current resource
    # "Product" => "product"
    # "Finance::Invoice" => 'invoice'
    # "Address::BillingAddress" => 'billing_address'
    def resource_key
      api_resource.name.demodulize.underscore
    end

    def default_scope
      api_resource
    end

    # Builds json serialization parameters based on
    # request parameters and resource configuration
    def build_json_parameters(build_params=nil)
      build_params ||= request.parameters
      json_parameters = {}
      json_parameters.merge!(:methods => build_params[:methods]) if build_params[:methods]
      json_parameters.merge!(:include => build_params[:include]) if build_params[:include]
      json_parameters.merge!(:except => build_params[:except]) if build_params[:except]
      json_parameters.merge!(:root => build_params[:root]) if build_params[:root]
      if [:create, :update, :find_or_create].include? build_params[:action].to_sym
        json_parameters[:methods] ||= []
        json_parameters[:methods] |= [:error_messages]
      end
      json_parameters.merge!(:root => resource_root) if resource_root
      return json_parameters
    end

    # builds scope for index calls
    # accepts initial scope as an argument
    # if initial_scope not present, uses default_scope for the current resource controller
    # adds pagination and joins and order by scopes based on request parameters
    # also takes custom params as argument
    # allows specifying defined scopes on resource using parameters
    # use custom_params or params to build score
    def build_scope(initial_scope=nil,build_params=nil)
      build_params ||= request.parameters
      build_params[:scopes] ||= {}
      join_params = get_join_params(build_params)
      scope = initial_scope ||= default_scope
      build_params[:per] ||= scope.count
      build_params[:page] ||= 1
      query_options = clean_params(build_params)[:query_options]
      build_params[:scopes].each do |scope_name,params|
       scope = params.present? ? scope.send(scope_name,params) : scope.send(scope_name)
      end
      scope = scope.select(build_params[:select]) if build_params[:select]
      scope = scope.joins(join_params) if join_params
      scope = scope.where(query_options) if query_options.present? && query_options != "{}"
      scope = scope.group(build_params[:group]) if build_params[:group]
      scope = scope.order(build_params[:order]) if build_params[:order]
      scope = scope.page(build_params[:page]).per(build_params[:per]) if build_params[:page] && build_params[:per]
      scope
    end

    # returns proper object for join queries
    # joins method needs symbolic strings for hashes, arrays or even a single string should be a symbol
    def get_join_params(build_params)
      return nil if !build_params[:joins].present?
      if build_params[:joins].is_a? String
        return build_params[:joins].to_sym
      elsif build_params[:joins].is_a? Hash
        return Hash[build_params[:joins].map{|key,value| [key.to_sym, value.to_sym] }]
      elsif build_params[:joins].is_a? Array
        return build_params[:joins].map(&:to_sym)
      end
      return nil
    end

    # Returns default root to be picked for json serialization
    # Override in including controllers where a custom root is needed
    def resource_root
      return nil
    end
    
    # convers blank params to nil
    # use it in case we need to put nil criteria while building scopes
    def clean_params(build_params=nil)
      build_params ||= request.parameters
      @clean_params ||= HashWithIndifferentAccess.new.merge blank_to_nil( build_params )
    end

    # recursively converts blank values in the provided hash into nil
    def blank_to_nil(hash)
      hash.inject({}){|h,(k,v)|
        h.merge(
          k => case v
          when Hash
            blank_to_nil v
          when Array
            v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
          else
            v == 'true' ? true : (v == 'false' ? false : (v == "" ? nil : (is_i?(v) ? v.to_i : v)))
          end
        )
      }
    end
    
    # Default before_save for create and update actions
    # This can be overridden to do specific actions before saving a resource
    def before_save
    end
    
    # converts string integer param to int
    # "32" => 32
    # Rails with postgres doesn't work with string values for integers in queries
    def is_i? (str_data)
       /\A[-+]?\d+\z/ === str_data
    end
end