Module: HasHelpers::ActiveRecord
- Includes:
- ActiveRecordUtils
- Defined in:
- lib/has_helpers/active_record.rb,
lib/has_helpers/active_record/base.rb,
lib/has_helpers/active_record/date_range.rb,
lib/has_helpers/active_record/validation.rb,
lib/has_helpers/active_record/nested_resource.rb
Defined Under Namespace
Modules: Base, DateRange, NestedResource, Validation Classes: ServiceError
Class Method Summary collapse
-
.relation_exists?(relation_name) ⇒ Boolean
TODO remove this in the future when elastic search or web services are implemented.
Instance Method Summary collapse
-
#add_joins(params, query, organization_id) ⇒ Object
In case we have a nested association and an organization_id , iterate over and add the neccessary joins.
-
#assign_data(data, prefix: nil, current_user: nil, multi: nil) ⇒ Object
This method assign the data considering associations and user context.
- #dn_columns ⇒ Object
- #get_column_type(column_or_attribute) ⇒ Object
-
#has_favorites(limit: nil, validate_proc: nil, limit_trigger: nil) ⇒ Object
Enable favorites.
- #has_recently_viewed(*resources) ⇒ Object
-
#ignore_columns ⇒ Object
An array of columns that should be ignored or excluded, by default is an empty array.
- #is_valid_association(association_values) ⇒ Object
-
#klass_available_columns ⇒ Object
returns the available columns for creating a new object of the model class.
- #klass_belongs_to_available_columns ⇒ Object
-
#lookup(params, organization_id, scope: nil, condition: LogicalOperator::AND, show_error: false, return_multiple: false, case_sensitive: true) ⇒ Class/nil/String
Lookup for models.
-
#lookup_attributes_precedence ⇒ Object
This is a list of attributes of the model that will be used to search by.
- #lookup_by_attributes_precedence(params, organization_id, case_sensitive, return_multiple) ⇒ Object
-
#organization_attribute_exist? ⇒ Boolean
Checks whether the lookup_attributes_precedence hash contains the key :organization_id.
-
#query_condition(params, case_sensitive, model = nil, condition = LogicalOperator::AND) ⇒ Object
Custom method for constructing AND/OR conditions based on input parameters params example: :name=>"Gayla Barrows".
-
#supported_columns_lookup(prefix = nil, multi = nil) ⇒ Object
Returns an array of valid columns for extraction search, such as advisor_id and advisor_import_key.
Methods included from ActiveRecordUtils
#key_with_prefix, #metadata_colums, #remove_prefix
Class Method Details
.relation_exists?(relation_name) ⇒ Boolean
TODO remove this in the future when elastic search or web services are implemented
512 513 514 |
# File 'lib/has_helpers/active_record.rb', line 512 def self.relation_exists?(relation_name) ::ActiveRecord::Base.connection.execute("SELECT 1 FROM #{ relation_name } LIMIT 1") rescue false end |
Instance Method Details
#add_joins(params, query, organization_id) ⇒ Object
In case we have a nested association and an organization_id , iterate over and add the neccessary joins
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/has_helpers/active_record.rb', line 170 def add_joins(params, query, organization_id) params.each do |key, _value| joins = key.to_s.split(".") if joins.size > 1 joins[0...-1].each do |join_table| association_klass = ::HasHelpers::ApplicationHelper.translate_klass_name(join_table.singularize.capitalize).safe_constantize table_name = self.reflect_on_association(join_table.singularize.to_sym)&.klass ? join_table.singularize.to_sym : join_table.to_sym # only singularize association if it's needed query = query.joins(table_name) query = query.where("#{join_table}.organization_id = (?)", organization_id) if organization_id.present? && association_klass.present? && association_klass.has_attribute?(:organization_id) end end end query = query.where(organization_id: organization_id) if organization_id.present? && params.any? && !organization_attribute_exist? query end |
#assign_data(data, prefix: nil, current_user: nil, multi: nil) ⇒ Object
This method assign the data considering associations and user context. returns a hash containing the hydrated model and any associations with errors.
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/has_helpers/active_record.rb', line 223 def assign_data(data, prefix: nil, current_user: nil, multi: nil) klass = self model = klass.new reflections = klass.reflections # has_many, belons_to, has_one date_columns = klass.columns.select { |column| column.type == :date }.map(&:name) # start_date, end_date columns = klass.column_names - reflections&.keys - date_columns # name, is_adjustment, note associations_with_errors = {} columns.each do |column| model[column] = data[key_with_prefix(column, prefix)] if data[key_with_prefix(column, prefix)].present? end date_columns.each do |column| model.assign_attributes({ column.to_sym => data[key_with_prefix(column, prefix)] }) if data[key_with_prefix(column, prefix)].present? end if current_user.present? model.organization = current_user&.organizations&.first model.created_by_id = current_user&.id if model.has_attribute?("created_by_id") model.updated_by_id = current_user&.id if model.has_attribute?("updated_by_id") end reflections.each do |association_name, reflection| result = nil if !.include?(association_name) association = reflection.[:class_name].present? ? reflection.[:class_name].safe_constantize : (reflection.klass || reflection.active_record) is_constant = association.include?(::HasHelpers::Constant) if is_constant if data[key_with_prefix("#{association_name}_id", prefix)].present? model.send("#{association_name}=", association.lookup(id: data[key_with_prefix("#{association_name}_id", prefix)])) else constant_value = data[key_with_prefix(association_name, prefix)] || data[key_with_prefix("#{association_name}_name", prefix)] # examples: policy_holder_type, policy_holder_type_id, policy_holder_type_name because the association name is only 'policy_holder_type' model.send("#{association_name}=", association.lookup(constant_value)) if constant_value.present? end else case reflection.macro when :belongs_to, :has_one association_data = remove_prefix(association, data, prefix.present? ? "#{prefix}_#{association_name}" : association_name) if association_data.any? result = association.lookup(association_data, model&.organization_id) result.nil? ? associations_with_errors[association_name] = association : model.send("#{association_name}=", result) end when :has_many if multi.present? (1..multi).each do |n| digit = n.humanize.underscore.despace association_name = association_name.singularize association_data = remove_prefix(association, data, prefix.present? ? "#{prefix}_#{association_name}_#{digit}" : "#{association_name}_#{digit}") if association_data.any? # TODO: improve for User model that no longer has organization_id result = model.has_attribute?(:organization_id) ? association.lookup(association_data, nil) : association.lookup(association_data, model&.organization_id) if result.present? model.send("#{association_name.pluralize}") << result else associations_with_errors[association] = association_name end end end end end end end end { model: model, associations_with_errors: associations_with_errors } end |
#dn_columns ⇒ Object
208 209 210 |
# File 'lib/has_helpers/active_record.rb', line 208 def dn_columns self.column_names.select { |item| item.end_with?("_dn") } end |
#get_column_type(column_or_attribute) ⇒ Object
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/has_helpers/active_record.rb', line 326 def get_column_type(column_or_attribute) model_name, column_name = column_or_attribute.to_s.split(".") if model_name.blank? || column_name.blank? return self.columns_hash[column_or_attribute]&.type end if model_name == self.table_name return self.columns_hash[column_name]&.type end reflection = self.reflections[model_name&.singularize] || self.reflections[model_name] class_name = reflection&.klass || model_name.classify.safe_constantize class_name&.columns_hash&.dig(column_name)&.type end |
#has_favorites(limit: nil, validate_proc: nil, limit_trigger: nil) ⇒ Object
Enable favorites
Examples
Create a favorite with the default limit trigger and validation:
has_favorites limit: 5
Create a favorite with a custom validation:
has_favorites validate_proc: ->(){ errors.add(:base, "Too many favorites") if limit > some_limit }
Create a favorite with a custom limit trigger: class LimitReportFavorites < ::HasHelpers::LimitFavorites def trigger_function <<-SQL CREATE OR REPLACE FUNCTION public."#function_name"() RETURNS TRIGGER LANGUAGE plpgsql AS $function$ DECLARE row_limit INTEGER; nrows INTEGER; resource_id TEXT; BEGIN row_limit := #limit;
SELECT
COUNT(*) INTO nrows
FROM favorites
INNER JOIN report.reports AS reports ON reports.id = favorites.report_id
WHERE favorites.created_by_id = NEW.created_by_id
AND reports.resource_id = (SELECT r.resource_id FROM report.reports AS r WHERE r.id = NEW.report_id);
IF nrows > row_limit THEN
SELECT
format('%s-%s', 'Report', NEW.report_id::text)
INTO resource_id;
RAISE NOTICE 'favorite limit: resource_id = %', resource_id;
RAISE 'Exceeded favorite limit of % for %', row_limit, resource_id;
END IF;
RETURN NULL;
END
$function$;
SQL
end
end
has_favorites limit_trigger: ::LimitReportFavorites.new(limit: 5)
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 |
# File 'lib/has_helpers/active_record.rb', line 483 def has_favorites(limit: nil, validate_proc: nil, limit_trigger: nil) class_name = self.name.demodulize association_name = class_name.underscore.to_sym foreign_key = class_name.foreign_key.to_sym has_many :favorites, class_name: "::HasHelpers::Favorite", dependent: :destroy ::HasHelpers::Favorite.belongs_to association_name, class_name: self.name, optional: true ::HasHelpers::Favorite.has_owner association_name if ::HasHelpers::Favorite.respond_to?(:has_owner) if limit ::HasHelpers::LimitFavorites.triggers << ::HasHelpers::LimitFavorites.new(klass: self, limit: limit) ::HasHelpers::Favorite.validate if: foreign_key do if ::HasHelpers::Favorite.where(created_by_id: created_by_id).where.not(foreign_key => nil).count >= limit errors.add(association_name, "Favorites are limited to #{limit} per #{association_name}.") end end end if validate_proc ::HasHelpers::Favorite.validate(if: foreign_key, &validate_proc) end if limit_trigger ::HasHelpers::LimitFavorites.triggers << limit_trigger end end |
#has_recently_viewed(*resources) ⇒ Object
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 |
# File 'lib/has_helpers/active_record.rb', line 343 def has_recently_viewed(*resources) ::HasHelpers.recently_viewed_resources = resources.map { |r| r.to_s.demodulize.tableize.to_sym } class_eval do # Returns the recently viewed records for the given association. # # Examples # class User < ActiveRecord::Base # has_recently_viewed :advisor, :policy, "HasOutlook::DiscussionTemplateConfig" # end # user = ::HasHelpers::User.find(id) # # user.recently_viewed(:advisor) # user.recently_viewed(:discussion_template_config) def recently_viewed(association_name) select_clauses = ["recently_vieweds.*"] join_clauses = [] resource_klass = ::HasHelpers::RecentlyViewed.reflect_on_association(association_name).klass resource_base_klass = resource_klass.base_class # Detect owner view unless resource_base_klass.instance_variable_defined?(:@owner_column_names) owner_relation_name = "owner_#{ resource_base_klass.table_name }_view" resource_base_klass.instance_variable_set(:@owner_relation_name, owner_relation_name) if resource_base_klass.respond_to?(:owner_klass) && resource_base_klass.owner_klass resource_base_klass.instance_variable_set(:@owner_column_names, resource_base_klass.owner_klass.column_names) elsif ::HasHelpers::ActiveRecord.relation_exists?(owner_relation_name) # Check if the owner view is present. # Detect column names for owner view. resource_base_klass.instance_variable_set( :@owner_column_names, Class.new(::ActiveRecord::Base) do self.table_name = owner_relation_name end.column_names ) else # No owner view detected so join in the resource records resource_base_klass.instance_variable_set(:@owner_relation_name, resource_base_klass.table_name) resource_base_klass.instance_variable_set( :@owner_column_names, (resource_base_klass.column_names & %w[id name username number type]) ) end end # Add select clauses for owner columns. owner_relation_name = resource_base_klass.instance_variable_get(:@owner_relation_name) resource_base_klass.instance_variable_get(:@owner_column_names).each do |owner_column| select_clauses << "#{ owner_relation_name }.#{ owner_column } AS owner_#{ owner_column }" end join_clauses << "INNER JOIN #{ owner_relation_name } ON #{ owner_relation_name }.id = recently_vieweds.#{ resource_base_klass.name.foreign_key }" type_scope = ::HasHelpers::RecentlyViewed.where(owner_relation_name => { type: resource_klass }) if resource_klass != resource_base_klass scope = ::HasHelpers::RecentlyViewed. select(select_clauses.join(", ")). joins(join_clauses.join(" ")). where(created_by_id: id). where(organization_id: organization_id). # this included so that we only include records from the user's primary organization where("recently_vieweds.#{ resource_base_klass.name.foreign_key } IS NOT NULL"). limit(::HasHelpers::HISTORY_MAX) # Needed because STI parent classes may have more records than the limit, up to number_of_descendant_classes * history_max. # type_scope may be nil or may scope to a sub-type, e.g. where(policies: { type: "Application" }) if type_scope scope = scope.merge(type_scope) end scope end # this association is only used for the destroy callback has_many :all_recently_viewed, dependent: :destroy, foreign_key: "created_by_id", class_name: "::HasHelpers::RecentlyViewed" end resources.each do |r| base_resource_klass = r.to_s.classify.constantize # Add association to the given resource class and all of its descendants. [base_resource_klass, *base_resource_klass.descendants].each do |resource_klass| resource_klass.class_eval do has_many :all_recently_viewed, dependent: :destroy, class_name: "::HasHelpers::RecentlyViewed", foreign_key: r.to_s.singularize.foreign_key end end ::HasHelpers::RecentlyViewed.class_eval do belongs_to r.to_s.demodulize.singularize.underscore.to_sym, class_name: r.to_s.classify, optional: true base_resource_klass.descendants.each do |descendant_klass| belongs_to descendant_klass.name.demodulize.singularize.underscore.to_sym, foreign_key: r.to_s.singularize.foreign_key, optional: true end end end end |
#ignore_columns ⇒ Object
An array of columns that should be ignored or excluded, by default is an empty array
204 205 206 |
# File 'lib/has_helpers/active_record.rb', line 204 def ignore_columns [] end |
#is_valid_association(association_values) ⇒ Object
133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/has_helpers/active_record.rb', line 133 def is_valid_association(association_values) # the association needs to be singular, example for Clients : "demographic.ssn" # the association would be "demographic" and the column name "ssn" validation = self association_values_without_last = association_values[0...-1] # Create a new array without the last element column_name = association_values.last association_values_without_last.each do |association| validation = self.reflect_on_association(association.singularize.to_sym)&.klass validation ||= self.reflect_on_association(association.to_sym)&.klass end validation&.column_names&.include?(column_name) end |
#klass_available_columns ⇒ Object
returns the available columns for creating a new object of the model class.
213 214 215 |
# File 'lib/has_helpers/active_record.rb', line 213 def klass_available_columns self.column_names + klass_belongs_to_available_columns - ignore_columns - dn_columns - end |
#klass_belongs_to_available_columns ⇒ Object
217 218 219 |
# File 'lib/has_helpers/active_record.rb', line 217 def klass_belongs_to_available_columns self.reflections.select { |_association_name, reflection| reflection.macro == :belongs_to }&.keys # client -> client_status, firm, organization end |
#lookup(params, organization_id, scope: nil, condition: LogicalOperator::AND, show_error: false, return_multiple: false, case_sensitive: true) ⇒ Class/nil/String
Lookup for models
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/has_helpers/active_record.rb', line 29 def lookup(params, organization_id, scope: nil, condition: LogicalOperator::AND, show_error: false, return_multiple: false, case_sensitive: true) model = nil if params.is_a? Hash params.each do |key, _value| association_values = key.to_s.split(".") if !self.has_attribute?(key) params = params.except(key) unless association_values.size > 1 && is_valid_association(association_values) end end if params.any? if condition == LogicalOperator::OR model = add_joins(params, self, organization_id) model = query_condition(params, case_sensitive, model, LogicalOperator::OR) end if condition == LogicalOperator::AND model = add_joins(params, self, organization_id) model = query_condition(params, case_sensitive, model) end end else model = lookup_by_attributes_precedence(params, organization_id, case_sensitive, return_multiple) end # scope example: [ # {:joins=>{:policies=>:advisors}, :where=>{:advisors=>{:npn=>11111529}}}, # {:joins=>{:policy_holders=>[{:policy=>{:writing_advisors=>:contract}}]}, :where=>{:"contracts.individual_number"=>"V1BUZ9J44XGJHF", :"contracts.corporate_number"=>"V1BUZ9J44XGJHF"}, :condition=>"or"}] if scope.present? scope.each do |item| # join tables if specified in the scope item model = model.joins(item[:joins]) if item[:joins].present? if item[:condition].present? && item[:condition] == LogicalOperator::OR model = query_condition(item[:where], case_sensitive, model, LogicalOperator::OR) else model ||= self model = model.where(item[:where]) # LogicalOperator::AND by default end end end if return_multiple ::ActiveRecord::Base.transaction do return model.presence || [] rescue => e ::ActiveRecord::Base.connection.rollback_db_transaction = "SQL error in #{__method__}: #{e.}" if show_error return else Rails.logger.info end [] end else ::ActiveRecord::Base.transaction do model = model.limit(2) unless model.nil? # Limiting the query to retrieve a maximum of 2 records for improved performance and to prevent unnecessary processing. model.present? && model.size == 1 ? model.first : (show_error ? (model.present? && model.size > 1 ? "Multiple found." : "Not found.") : nil) rescue => e # In case of errors for example when we iterate over the lookup_attributes_precedence and try to match # an string with an uuid (id), avoid the error and continue looking for the next attribute ::Rails.logger.info "SQL error in #{__method__}: #{e.}" ::ActiveRecord::Base.connection.rollback_db_transaction nil end end end |
#lookup_attributes_precedence ⇒ Object
This is a list of attributes of the model that will be used to search by. Each model should override this method to lookup by simple attribute. e.g. Advisor.lookup("123456", 4) where "123456" is the unique import_key Note that nested associations should be defined as: table_name.attribute e.g. demographics.ssn Example for Advisor -> lookup_attributes_precedence = { id: "id", import_key: "import_key", code_name: "demographics.code_name", ssn: "demographics.ssn"}
191 192 193 194 195 |
# File 'lib/has_helpers/active_record.rb', line 191 def lookup_attributes_precedence @lookup_attributes_precedence ||= { id: "id" } end |
#lookup_by_attributes_precedence(params, organization_id, case_sensitive, return_multiple) ⇒ Object
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/has_helpers/active_record.rb', line 94 def lookup_by_attributes_precedence(params, organization_id, case_sensitive, return_multiple) model = nil value = params self.lookup_attributes_precedence.each do |_key, lookup_attribute| model_found = false joins = lookup_attribute.split(".") query = self # We don’t add organization_id in the query if organization_id is present in lookup_attributes_precedence, because organization_id is no longer in users. query = query.where(organization_id: organization_id) if organization_id.present? && !organization_attribute_exist? if joins.size > 1 joins[0...-1].each do |join_table| table_name = self.reflect_on_association(join_table.singularize.to_sym)&.klass ? join_table.singularize.to_sym : join_table.to_sym # only singularize association if it's needed query = query.joins(table_name) # the join need to be singularized and the where query uses the full string as 'demographics.ssn' end end if case_sensitive || lookup_attribute.to_s.include?("id") || [:text, :string].exclude?(get_column_type(lookup_attribute)) model = query.where("#{lookup_attribute} = :param_value", param_value: value.to_s) else model = query.where("LOWER(#{lookup_attribute}) = :param_value", param_value: value.to_s.downcase) end ::ActiveRecord::Base.transaction do model = model.limit(2) if model && !return_multiple # Limiting the query to retrieve a maximum of 2 records for improved performance and to prevent unnecessary processing. model_found = true if model.length > 0 rescue => e # In case of errors for example when we iterate over the lookup_attributes_precedence and try to match # an string with an uuid (id), avoid the error and continue looking for the next attribute ::Rails.logger.info "SQL error in #{__method__}: #{e.}" ::ActiveRecord::Base.connection.rollback_db_transaction model = nil model_found = false end break if model_found end model end |
#organization_attribute_exist? ⇒ Boolean
Checks whether the lookup_attributes_precedence hash contains the key :organization_id. Returns true if the key exists
199 200 201 |
# File 'lib/has_helpers/active_record.rb', line 199 def organization_attribute_exist? self.lookup_attributes_precedence.key?(:organization_id) end |
#query_condition(params, case_sensitive, model = nil, condition = LogicalOperator::AND) ⇒ Object
Custom method for constructing AND/OR conditions based on input parameters params example: :name=>"Gayla Barrows"
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/has_helpers/active_record.rb', line 298 def query_condition(params, case_sensitive, model = nil, condition = LogicalOperator::AND) table_name = self.table_name conditions = [] query_params = {} params.each do |column, value| column_name_query = column.to_s.tr(".", "_") # convert column names to a standardized format for consistency example: organizations.id -> :organizations_id, demographic.ssn -> :demographic_ssn association_values = column.to_s.split(".") # If the column is part of an association (e.g., demographics.name), # use the column name as it is. If it's not an association (e.g., name, id, import_key), # set the table_column as "#{table_name}.#{column}" (e.g., users.name, users.id, users.import_key). table_column = association_values.size > 1 ? column : "#{table_name}.#{column}" if case_sensitive || column.to_s.include?("id") || [:text, :string].exclude?(get_column_type(table_column)) conditions << "#{table_column} = :#{column_name_query}" else conditions << "LOWER(#{table_column}) = :#{column_name_query}" end # a hash of query parameters for use in the WHERE clause {:organizations_id=>1, :users_name=>"Naomi'Lynch"} query_params[column_name_query.to_sym] = case_sensitive ? value : value.to_s.downcase end query = conditions.join(" #{condition} ") # "organizations.id = :organizations_id OR users.name = :users_name" model.nil? ? where(query, query_params) : model.where(query, query_params) end |
#supported_columns_lookup(prefix = nil, multi = nil) ⇒ Object
Returns an array of valid columns for extraction search, such as advisor_id and advisor_import_key. The optional 'prefix' parameter appends a prefix to each column name (prefix + lookup attribute precedence), e.g., advisor_id, client_id, firm_id. When multi is set, the function generates columns with instance numbers, e.g., advisor_one_id, advisor_two_id, up to multi (advisor_multi_id).
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/has_helpers/active_record.rb', line 151 def supported_columns_lookup(prefix = nil, multi = nil) @prefix_supported_columns ||= {} @prefix_supported_columns ["#{prefix}, #{multi}"] ||= begin keys_without_prefix = self.lookup_attributes_precedence.keys return prefix.present? ? keys_without_prefix.map { |column| [prefix, column].join("_") } : keys_without_prefix.map(&:to_s) unless multi keys_supported_columns = [] (1..multi).each do |n| digit = n.humanize.underscore.despace keys_without_prefix.each do |key| keys_supported_columns << [prefix, digit, key].compact.join("_") end end keys_supported_columns end end |