Last active
November 4, 2019 07:16
-
-
Save prcongithub/90b58618553864b3a308d7c14ebf7b06 to your computer and use it in GitHub Desktop.
Simple Lightweight module for clean, dynamic and highly optimised API controllers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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