Skip to content

Instantly share code, notes, and snippets.

@prcongithub
Last active November 4, 2019 07:16
Show Gist options
  • Save prcongithub/90b58618553864b3a308d7c14ebf7b06 to your computer and use it in GitHub Desktop.
Save prcongithub/90b58618553864b3a308d7c14ebf7b06 to your computer and use it in GitHub Desktop.
Simple Lightweight module for clean, dynamic and highly optimised API controllers
require 'active_support'
module CRUDActions
extend ActiveSupport::Concern
included do
before_action :set_resource, only: [:show, :update, :destroy]
after_action :set_response_headers, only: [:index]
end
# Returns count of records matching the scope
def count
render :status => :ok, :json => { :count => build_scope.count }
end
# Returns count of records matching the scope for each of the unique group_by fields
# pass a group column as a quey param to get counts for each of the unique column values
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 = apply_pagination(build_scope)
render json: @resources.all.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
@resources = build_scope
@resources.update_all(create_params[:update_attributes])
render json: @resources.to_json(build_json_parameters)
end
# DELETE /resources/1
def destroy
@resource.destroy
end
# Destroys all records in the provided scope
# Use with caution
def destroy_all
table_name = api_resource.table_name
begin
scope = build_scope
count = scope.count
scope.delete_all
render :status => :ok, :json => { message: "#{count} records deleted" }.to_json
rescue Exception => exception
render :status => :invalid_request, :json => { message: exception.message }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_resource
@resource = build_scope.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!(:only => build_params[:only]) if build_params[:only]
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
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
end
def apply_pagination(scope)
scope = scope.page(params[:page]).per(params[:per])
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
def set_response_headers
if @resources.respond_to? :total_count
response.headers["X-Pagination"] = {
total_count: @resources.total_count(:id, :distinct => true),
total_pages: @resources.total_pages,
offset_value: @resources.offset_value,
per: (request.parameters[:per] || api_resource.default_per_page),
current_page: @resources.current_page
}.to_json
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment