module AlphaApi::Concerns::Actionable
Public Instance Methods
create()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 8 def create authorize! :create, resource_class new_resource = build_resource(permitted_create_params) if new_resource.valid? authorize! :create, new_resource new_resource.save render status: :created, json: resource_serializer.new(new_resource).serializable_hash else errors = reformat_validation_error(new_resource) raise Exceptions::ValidationErrors.new(errors), 'Validation Errors' end end
destroy()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 57 def destroy if destroyable resource = resource_class.find(params[:id]) authorize! :destroy, resource if resource.destroy head :no_content else raise Exceptions::ValidationErrors.new(resource.errors), 'Validation Errors' end else raise Exceptions::MethodNotAllowed, 'Method Not Allowed' end end
index()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 21 def index authorize! :read, resource_class query = apply_filter_and_sort(collection) apply_pagination if params[:page].present? records = paginate(query) records = records.padding(params[:page][:offset]) if params[:page][:offset] else records = query end options = options(nested_resources, params[:page], query.count) render json: resource_serializer.new(records, options).serializable_hash end
show()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 36 def show resource = resource_class.find(params[:id]) authorize! :read, resource options = options(nested_resources) render json: resource_serializer.new(resource, options).serializable_hash end
update()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 43 def update cached_resource_class = resource_class resource = cached_resource_class.find(params[:id]) authorize! :update, resource options = options(nested_resources) if resource.update(permitted_update_params(resource)) updated_resource = cached_resource_class.find(params[:id]) render json: resource_serializer.new(updated_resource, options).serializable_hash else errors = reformat_validation_error(resource) raise Exceptions::ValidationErrors.new(errors), 'Validation Errors' end end
Protected Instance Methods
allow_all()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 169 def allow_all false end
allowed_associations()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 77 def allowed_associations [] end
allowed_sortings()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 81 def allowed_sortings [] end
apply_combined_filters(query)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 118 def apply_combined_filters(query) conditions = [] filterable_fields.each do |field| value = params.dig(:filter, field) type = field_type(field) next unless value.present? && type.present? if type == :uuid || valid_enum?(field, value) query = query.where(field => value) elsif type == :string query = query.where(%("#{resource_class.table_name}"."#{field}" ILIKE #{sanitise(value)})) elsif valid_boolean?(field, value) query = query.where(field => value == 'true') else raise Exceptions::InvalidFilter, 'Only type of string and uuid fields are supported' end end query end
apply_filter(query)
click to toggle source
@override customised filters
# File lib/alpha_api/concerns/actionable.rb, line 93 def apply_filter(query) if filterable_fields.empty? raise Exceptions::InvalidFilter, 'Filters are not supported for this resource type' else query end end
apply_filter_and_sort(query)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 85 def apply_filter_and_sort(query) query = apply_standard_filter(query) if fields_filter_required? # custom filters query = apply_filter(query) if params[:filter] query = apply_sorting(query) end
apply_pagination()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 148 def apply_pagination page_number = (params.dig(:page, :number) || 1).to_i page_offset = (params.dig(:page, :offset) || 0).to_i page_size = (params.dig(:page, :size) || 20).to_i if allow_all && page_size == -1 params[:page] = nil else raise Exceptions::InvalidRequest, 'Page number must be positive' unless page_number.positive? raise Exceptions::InvalidRequest, 'Page offset must be non-negative' if page_offset.negative? raise Exceptions::InvalidRequest, 'Page size must be positive' unless page_size.positive? raise Exceptions::InvalidRequest, 'Page size cannot be greater than 100' if page_size > 100 params[:page] = { number: page_number, offset: page_offset, size: page_size } end end
apply_search_term(query, search_term)
click to toggle source
only override this method when filterable_fields
is not empty
# File lib/alpha_api/concerns/actionable.rb, line 110 def apply_search_term(query, search_term) # exclude all _id fields for OR query conditions = filterable_fields.select { |field| field_type(field) == :string }.map do |field| %("#{resource_class.table_name}"."#{field}" ILIKE #{sanitise(search_term)}) end query = query.where(conditions.join(' OR ')) end
apply_sorting(query)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 173 def apply_sorting(query) sort_params = params['sort'] return query.order(default_sorting) unless sort_params.present? raise Exceptions::InvalidRequest, 'Sort parameter must be a string' unless sort_params.is_a? String sorting = [] sorts = sort_params.split(',').map(&:strip).map do |sort| is_desc = sort.start_with?('-') sort = is_desc ? sort[1..-1] : sort raise Exceptions::InvalidRequest, "Sorting by #{sort} is not allowed" unless allowed_sortings.include?(sort.to_sym) sort = association_mapper(sort) # have to includes the association to be able to sort on association = sort.split('.')[-2] query = query.includes(association.to_sym) if association sorting << sort_clause(sort, is_desc ? 'DESC NULLS LAST' : 'ASC NULLS FIRST') end query.order(sorting.join(',')) end
apply_standard_filter(query)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 137 def apply_standard_filter(query) return query if filterable_fields.empty? # generate where clauses of _contains search_term = params[:search_term] query = if search_term.present? apply_search_term(query, search_term) else apply_combined_filters(query) end end
association_mapper(sort)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 195 def association_mapper(sort) components = sort.split('.') return sort if components.length == 1 mapper = { 'reseller' => 'organisation' } table_name = components[-2] "#{mapper[table_name] || table_name}.#{components[-1]}" end
build_resource(resource_params)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 205 def build_resource(resource_params) resource_class.new(resource_params) end
collection()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 209 def collection resource_class.accessible_by(current_ability) end
default_sorting()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 240 def default_sorting { created_at: :desc } end
destroyable()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 73 def destroyable false end
fields_filter_required?()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 105 def fields_filter_required? (params[:search_term] || params[:filter]) && filterable_fields.present? end
filterable_fields()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 101 def filterable_fields [] end
nested_resources()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 244 def nested_resources nested_resources = params[:include].to_s.split(',') invalid_resources = [] nested_resources.each { |res| invalid_resources.push(res) unless allowed_associations.include?(res.to_sym) } unless invalid_resources.empty? raise Exceptions::InvalidArgument, "Invalid value for include: #{invalid_resources.join(', ')}" end nested_resources end
options(included, page = nil, count = nil)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 255 def options(included, page = nil, count = nil) options = { include: included, params: { included: included } } options[:meta] = {} options[:meta][:total_count] = count if count options[:meta][:page_number] = page[:number] if page options[:meta][:page_size] = page[:size] if page options end
permitted_create_params()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 269 def permitted_create_params data = params.require(:data) data.require(:attributes) unless data.include?(:attributes) data[:attributes] end
permitted_update_params(_resource)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 275 def permitted_update_params(_resource) data = params.require(:data) data.require(:attributes) unless data.include?(:attributes) data[:attributes] end
reconcile_item(existing_items, item)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 281 def reconcile_item(existing_items, item) item_id = item['id'] if item_id && !existing_items.find { |i| i.id == item_id } # Any unreconciled items in the update need to be re-created item.except(:id) else item end end
reconcile_nested_attributes(existing_items, items_in_update)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 213 def reconcile_nested_attributes(existing_items, items_in_update) item_ids_in_update = items_in_update.map { |item| item['id'] }.compact if item_ids_in_update.uniq.length != item_ids_in_update.length raise(Exceptions::InvalidRequest, 'Nested attribute IDs must be unique') end nested_attributes = [] items_in_update.each do |item| nested_attributes << reconcile_item(existing_items, item) end # Existing item was not found in updated items, so should be deleted existing_items.reject { |existing| item_ids_in_update.include?(existing.id) }.each do |deleting_item| nested_attributes << { 'id': deleting_item.id, '_destroy': true } end nested_attributes end
reformat_validation_error(resource)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 314 def reformat_validation_error(resource) resource.errors end
resource()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 236 def resource resource_class.find(params[:id]) end
resource_class()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 318 def resource_class controller_name.classify.constantize end
resource_serializer()
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 322 def resource_serializer "Api::V1::#{controller_name.classify}Serializer".constantize end
sort_clause(sort, order)
click to toggle source
e.g. sort: 'user.organisation', order: 'desc'
# File lib/alpha_api/concerns/actionable.rb, line 327 def sort_clause(sort, order) components = sort.split('.') attr_name = components[-1] if components.length == 1 # direct attributes "#{resource_class.table_name}.#{attr_name} #{order}" elsif components.length == 2 # direct association attributes association = resource_class.reflect_on_association(components[-2]) "#{association.table_name}.#{attr_name} #{order}" else # could potencially support that as well by includes deeply nested associations raise Exceptions::InvalidRequest, 'Sorting on deeply nested association is not supported' end end
Private Instance Methods
field_type(field)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 345 def field_type(field) resource_class.attribute_types[field.to_s].type end
sanitise(str)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 349 def sanitise(str) ActiveRecord::Base.connection.quote("%#{str}%") end
valid_boolean?(field, value)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 353 def valid_boolean?(field, value) field_type(field) == :boolean && ['true', 'false'].include?(value) end
valid_enum?(field, value)
click to toggle source
# File lib/alpha_api/concerns/actionable.rb, line 357 def valid_enum?(field, value) enum = resource_class.defined_enums[field.to_s] enum ? enum.keys.include?(value) : false end