# frozen_string_literal: true class ApplicationRecord < ActiveRecord::Base primary_abstract_class module ApiMethods extend ActiveSupport::Concern def as_json(options = {}) options ||= {} options[:except] ||= [] options[:except] += hidden_attributes options[:methods] ||= [] options[:methods] += method_attributes super(options) end def serializable_hash(*args) hash = super(*args) hash.transform_keys { |key| key.delete("?") } end protected def hidden_attributes %i[uploader_ip_addr updater_ip_addr creator_ip_addr user_ip_addr ip_addr] end def method_attributes [] end end module UserMethods def belongs_to_creator(options = {}) field = options.delete(:field) || :creator class_eval do belongs_to(field, **options.merge(class_name: "APIUser")) before_validation(on: :create) do |rec| rec.send("#{field}_id=", CurrentUser.id) if rec.send("#{field}_id").nil? rec.send("#{field}_ip_addr=", CurrentUser.ip_addr) if rec.respond_to?(:"#{field}_ip_addr=") && rec.send("#{field}_ip_addr").nil? end end end end module SearchMethods def attribute_matches(attribute, value, **) return all if value.nil? column = column_for_attribute(attribute) case column.sql_type_metadata.type when :boolean boolean_attribute_matches(attribute, value, **) when :integer, :datetime numeric_attribute_matches(attribute, value, **) when :string, :text text_attribute_matches(attribute, value, **) when :uuid where(attribute => value) else raise(ArgumentError, "unhandled attribute type: #{column.sql_type_metadata.type}") end end def boolean_attribute_matches(attribute, value) if value.to_s.truthy? value = true elsif value.to_s.falsy? value = false else raise(ArgumentError, "value must be truthy or falsy") end where(attribute => value) end # range: "5", ">5", "<5", ">=5", "<=5", "5..10", "5,6,7" def numeric_attribute_matches(attribute, range) column = column_for_attribute(attribute) qualified_column = "#{table_name}.#{column.name}" parsed_range = ParseValue.range(range, column.type) add_range_relation(parsed_range, qualified_column) end def add_range_relation(arr, field) return all if arr.nil? case arr[0] when :eq if arr[1].is_a?(Time) where("#{field} between ? and ?", arr[1].beginning_of_day, arr[1].end_of_day) else where(["#{field} = ?", arr[1]]) end when :gt where(["#{field} > ?", arr[1]]) when :gte where(["#{field} >= ?", arr[1]]) when :lt where(["#{field} < ?", arr[1]]) when :lte where(["#{field} <= ?", arr[1]]) when :in where(["#{field} in (?)", arr[1]]) when :between where(["#{field} BETWEEN ? AND ?", arr[1], arr[2]]) else all end end def text_attribute_matches(attribute, value, convert_to_wildcard: false) column = column_for_attribute(attribute) qualified_column = "#{table_name}.#{column.name}" value = "*#{value}*" if convert_to_wildcard && value.exclude?("*") if value =~ /\*/ where("lower(#{qualified_column}) LIKE :value ESCAPE E'\\\\'", value: value.downcase.to_escaped_for_sql_like) else where("to_tsvector(:ts_config, #{qualified_column}) @@ plainto_tsquery(:ts_config, :value)", ts_config: "english", value: value) end end def apply_basic_order(params) case params[:order] when "id_asc" order(id: :asc) when "id_desc" order(id: :desc) else default_order end end def default_order order(id: :desc) end def search(params) params ||= {} q = all q = q.attribute_matches(:id, params[:id]) q = q.attribute_matches(:created_at, params[:created_at]) if attribute_names.include?("created_at") q = q.attribute_matches(:updated_at, params[:updated_at]) if attribute_names.include?("updated_at") q end end include ApiMethods extend SearchMethods extend UserMethods end