module Dunlop::Ember::ApiBaseController extend ActiveSupport::Concern included do cattr_accessor :predicate_end_matcher before_action :authenticate_user_from_token! before_action :authenticate_user! before_action :set_current_user before_action :find_record, only: %i[show update destroy] respond_to :json, :csv rescue_from CanCan::AccessDenied do render json: {errors: [{status: 403, detail: 'unauthorized'}]}, status: 403 end rescue_from ActiveRecord::RecordNotFound do render json: {data: nil}, status: 404 end end def index index_safeguard record_class result = record_scope.search(query).result result = result.includes(*FlatKeys.as_nested_structure(index_include_relations)) if index_include_relations.present? result = result.uniq if params[:uniq].present? if index_pagination? paginated = result.page(params[:page]).per(params[:per_page]) render json: paginated, meta: index_meta_params.merge(total_pages: paginated.total_pages, total_count: paginated.total_count) else render json: result, meta: index_meta_params end end def show show_safeguard @record render json: @record end def create @record ||= record_class.new(record_params) create_safeguard @record return if performed? if @record.save render json: @record else render json: {errors: @record.errors.map{|attr, errors| {attr => errors.to_s}}}, status: :unprocessable_entity end end def update update_safeguard @record return if performed? @record.update_attributes record_params render json: @record end def destroy destroy_safeguard @record @record.destroy head :no_content end private # used to include relations on database lookup # has form: # ['comments.replies', 'authors.orginizations'] # def include_relations return [] unless includes = params[:include].presence includes.split(',') end %i[index show].each do |action| define_method "#{action}_include_relations" do include_relations end end def publish(event, data = {}) return data = data.merge(event: event) data[:client_id] = request.headers['HTTP_CLIENT_ID'] if request.present? FayeRails.client(nil).publish('/activity', data) end def index_safeguard(model) # can be overloaded end def show_safeguard(record) # can be overloaded record_safeguard(record) end def create_safeguard(record) # can be overloaded record_safeguard(record) end def update_safeguard(record) # can be overloaded record_safeguard(record) end def destroy_safeguard(record) # can be overloaded record_safeguard(record) end def record_safeguard(record) # can be overloaded return render json: {}, status: :unauthorized unless authorized_ids(record).include?(current_user.id) end def authorized_ids(*) # can be overloaded [] end def authenticate_user_from_token! # https://romulomachado.github.io/2015/09/28/using-ember-simple-auth-with-devise.html authenticate_with_http_token do |token, options| user_email = options[:email].presence user = user_email && User.find_by_email(user_email) if user && Devise.secure_compare(user.authentication_token, token) sign_in user, store: false end end end public def record_class deducted_class_name = controller_path.sub(/api\//,'').classify deducted_class_name.safe_constantize || deducted_class_name.demodulize.constantize end # basic scope for find and index. Can be overloaded to add extra security restrictions def record_scope record_class.all end def find_record(options = {}) scope = record_scope scope = scope.includes(*Array.wrap(options[:includes])) if options[:includes] @record = scope.find(params[:id]) end def record_params #params.require(controller_path.sub(/^api\//, '').singularize).permit(*permitted_params) params.require(controller_path.sub(/api\//, '').singularize).permit(*permitted_params) end def permitted_params [] end # used for serialization def serialize_options return {} unless includes = params[:include].presence {include: includes.split(',')} end # map action specific serialize options to general serialize options %i[index show create update].each do |action| define_method "#{action}_serialize_options" do serialize_options end end # This method can be overloaded by the including controller to enrich the meta information of the collection # with other meta-data def index_meta_params {} end # predicate_end_matcher => # /(_eq|_eq_any|_eq_all|_not_eq|_not_eq_any|_not_eq_all|_matches|_matches_any|_matches_all| # _does_not_match|_does_not_match_any|_does_not_match_all|_lt|_lt_any|_lt_all|_lteq|_lteq_any| # _lteq_all|_gt|_gt_any|_gt_all|_gteq|_gteq_any|_gteq_all|_in|_in_any|_in_all|_not_in|_not_in_any| # _not_in_all|_cont|_cont_any|_cont_all|_i_cont|_i_cont_any|_i_cont_all|_not_cont|_not_cont_any| # _not_cont_all|_i_not_cont|_i_not_cont_any|_i_not_cont_all|_start|_start_any|_start_all|_not_start| # _not_start_any|_not_start_all|_end|_end_any|_end_all|_not_end|_not_end_any|_not_end_all| # _true|_not_true|_false|_not_false|_present|_blank|_null|_not_null)$/ #TODO: make me pretty, specs are in place def query self.class.predicate_end_matcher ||= Regexp.new("(_#{Ransack.predicates.keys.join('|_')})$") mapped_query = {} (params[:q] || {}).each do |key, value| key = "#{key}_eq" unless %w[s sorts].include?(key) or record_class.try(:ransackable_scopes).try(:include?, key.to_sym) or key =~ self.class.predicate_end_matcher # expect equality when no predicate is given if key =~ /^type_/ # map type to sti_type mapped_value = case value when Array then value.map{|v| "#{record_class.name}::#{v.to_s.gsub('-', '_').classify}"} else "#{record_class.name}::#{value.to_s.gsub('-', '_').classify}" end mapped_query["sti_#{key}"] = mapped_value else mapped_query[key] = value end end mapped_query end def _render_with_renderer_json(resource, options = {}) if resource == nil return params[:id].blank? ? '{"data":[]}' : '{"data":null}' end return resource.to_json if resource.is_a?(Hash) return resource if resource.is_a?(String) return resource.to_json if resource.try(:custom_data?) action_serialize_options_method = :"#{action_name}_serialize_options" #action_serialize_options = respond_to?(action_serialize_options_method) ? __send__(action_serialize_options_method) : {} action_serialize_options = __send__(action_serialize_options_method) rescue {} action_serialize_options[:include] = options[:include] if options.has_key?(:include) action_serialize_options[:meta] = options[:meta] if options.has_key?(:meta) serializer_class = if options.has_key?(:serializer) options.delete(:serializer) elsif action_serialize_options[:serializer].present? action_serialize_options[:serializer] elsif resource.is_a?(ActiveRecord::Base) #infer based on controller path replacing actual controller part with resouce part /workflow_step_definitions/:id/table "#{self.class.name.deconstantize}::#{resource.class.name.demodulize}Serializer".constantize elsif resource.is_a?(Array) and resource.first.is_a?(ActiveRecord::Base) # infer based in first element of collection "#{self.class.name.deconstantize}::#{resource.first.class.name.demodulize}Serializer".constantize else # infer based on controller path "#{controller_path.underscore.classify}Serializer".constantize end serializer = if resource.respond_to?(:each) ActiveModel::Serializer::CollectionSerializer.new(resource, action_serialize_options.merge(serializer: serializer_class)) #TODO: fix index includes else serializer_class.new(resource, action_serialize_options) end adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer, action_serialize_options) res = {} time = Benchmark.ms { res = adapter.as_json } logger.info "Generated serialized output in #{time.round} ms" #binding.pry #return res return res.is_a?(String) ? res : res.to_json end def has_permission_of_type(type, user = current_user) Dunlop::Ability.has_permission_of_type(type, user) end def any_permission_starts_with(expression, user = current_user) Dunlop::Ability.any_permission_starts_with(expression, user) end def index_pagination? true end def set_current_user request.env["exception_notifier.exception_data"] = { current_user: current_user.try(:inspect) } User.current = current_user end end