Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- require 'dm-aggregates'
- require 'lib/sbfapi/data_mapper_db'
- require 'lib/sbfapi/errors/sbf_error'
- require 'v2/lib/sbf_error'
- require 'v2/services/lib/base'
- require 'lib/sbfapi/log'
- require 'wisper'
- module V2
- # A base service to provide helper and utility methods to service classes
- class OrmService < V2::BaseService
- extend Wisper::Publisher
- @entity_class = nil
- # Get a reference to the entity class associated with this controller (assuming things are named correctly)
- #
- def self.entity_class
- @entity_class ||= const_get(name.deconstantize).const_get(name.demodulize.rpartition('Service').first)
- end
- private_class_method :entity_class
- def self.default_tenant
- consumer_id = RequestSession[:consumer_id] || 0
- profile_id = RequestSession[:profile_id] || 0
- is_admin = RequestSession[:is_admin] || false
- V2::Tenant.new(consumer_id: consumer_id, profile_id: profile_id, is_admin: is_admin)
- end
- private_class_method :default_tenant
- # rubocop:disable BlockComments
- =begin
- # TODO:
- #
- # Yes, commented out code is BAD. However we have intentions of implmenting a generic route
- # in the future and we believe this code is that foundation that the code will be built on.
- # Therefore we are leaving the commented out code here so that we don't have to figure this
- # all out again in the future.
- def self.rank(field = nil, filter = nil, order = nil)
- collection = entity_class.all
- unless filter.empty?
- filter = parse_filter(filter)
- collection = recursively_apply_filter(filter, collection)
- conditions_statement, bind_values = collection.query.repository.adapter.send(:conditions_statement, collection.query.conditions, true)
- end
- collection = collection.by_sql(entity_class) { |entity, entity2|
- bind_variables = []
- statement = []
- fields = field.split('.')
- if fields.count == 1
- entity2.model_class.
- tmp = "SELECT (COUNT(*) FROM #{entity2} WHERE (#{conditions_statement}) AND
- #{entity2.send(fields[0].to_sym)} > #{entity.send(fields[0].to_sym)}) + 1 AS rank
- FROM #{entity} WHERE #{conditions_statement}"
- statement << tmp
- elsif fields.count == 2
- else
- raise V2::SBFBadRequestError, "Improper field was passed in. Can only rank one field deep."
- end
- # Join on participant to get the team captain.
- inner_join, inner_binds = team.model_class.build_inner_join([:event])
- statement << inner_join
- bind_variables.push(*inner_binds)
- bind_variables.push(*bind_values) unless bind_values.nil?
- #statement << "WHERE (#{conditions_statement})"
- #statement << "ORDER BY #{team.team_name} ASC"
- [statement.join(' '), *bind_variables]
- }
- collection
- end
- =end
- # rubocop:enable BlockComments
- # Creates an object using the input data
- #
- # @option data [Hash] Hash of parameters to be updated
- #
- # @raise [V2::SBFMalformedRequestError] if one of the required params is not passed in
- # @raise [V2::SBFBadRequestError] if one of the params passed in is not good
- # @raise [V2::SBFSaveError] if the object could not be created
- #
- def self.create(params, skip_deduplicator = false)
- # Prune out relationship information
- parent_data, child_data = prune_parents_and_children(params)
- # Make sure the params are valid
- ensure_valid_create_params(params, parent_data)
- # Create new class instance of the entity
- entity = entity_class.new(params)
- entity_class.transaction do
- # Create/update relationships and set them on the entity
- prepare_parent_relationships(entity, parent_data)
- # Ensure the policy allows tenant to create the entity
- authorize_create(entity, default_tenant)
- # Call generic save method
- entity = skip_deduplicator ? V2::ProfileService.save(entity, skip_deduplicator) : save(entity)
- # Create/update relationships and set them on the entity
- prepare_child_relationships(entity, child_data)
- # Ensure the policy allows tenant to update the entity
- authorize_update(entity, default_tenant)
- # Call save again in case the setting of relationship made the entity dirty
- skip_deduplicator ? V2::ProfileService.save(entity, skip_deduplicator) : save(entity)
- end
- end
- # Returns true if the params suggest that the entity already exists
- # (Checks to see if the key fields have been specified)
- #
- def self.needs_create?(params, clazz = entity_class)
- !params_present_and_set?(clazz.keys, params)
- end
- # Errors if required fields are missing or an invalid column is found
- #
- def self.ensure_valid_create_params(params, pruned = {}, allow_serial = false)
- # Don't allow a user to set any serial fields
- params.reject! { |k, _| entity_class.serial?(k) } unless allow_serial
- # We need to allow for required parameters which have a default value configured
- required_properties_without_defaults = entity_class.requireds - entity_class.columns_with_default_values
- # Make sure all required parameters are present
- missing = required_properties_without_defaults.select { |param| !params.include?(param) }
- #If a required property is not present but the association it is related to is
- special_properties = missing.select do |param|
- parts = param.to_s.split('_')
- parts.pop
- property = parts.join('_').to_sym
- pruned.key?(property) && !(pruned[property].nil?)
- end
- missing -= special_properties
- raise(V2::SBFMalformedRequestError, details: 'Missing required parameter(s)', errors: missing.to_a) unless missing.empty?
- # Make sure all required parameters are non-empty
- unset = required_properties_without_defaults.select { |param| params[param].empty? }
- unset -= special_properties
- raise(V2::SBFBadRequestError, details: 'Required parameter(s) must be set', errors: unset.to_a) unless unset.empty?
- # Also make sure that all parameters are valid columns
- check_params_valid(entity_class.:columns, params)
- end
- private_class_method :ensure_valid_create_params
- # Saves the entity and properly formats any errors
- #
- def self.save(entity, skip_deduplication = false)
- # Make sure we have changed something
- unless entity.dirty?
- LOG.info { "Ignoring save on #{entity.class.name.demodulize} because no attributes have changed" }
- return entity
- end
- LOG.debug {
- dirty_attributes = entity.dirty_attributes.map { |k, v| "#{k.name} => #{v}" }
- "Saving #{entity.class.name.demodulize}, fields [ #{dirty_attributes.join(', ')} ]"
- }
- # Execute the create in a transaction
- begin
- entity.new? ? authorize_create(entity, default_tenant) : authorize_update(entity, default_tenant)
- # Save will return false if it fails for any reason
- unless skip_deduplication ? V2::ProfileService.save(entity, skip_deduplication) : entity.save
- raise V2::SBFBadRequestError, "Could not save #{entity.class.name.demodulize}: #{entity.collect_errors.join('. ')}"
- end
- rescue V2::SBFError => sbfe
- raise sbfe
- rescue => e
- LOG.error { "Error saving database entity: #{e}\n#{e.backtrace[0..10].join("\n")}" }
- raise V2::SBFSaveError, "Could not save #{entity.class.name.demodulize}"
- end
- # Reload the entity to get the value of any defaulted parameters
- entity.reload
- end
- # Gets an object using the input data
- #
- # @option key_field_hash [Hash] {key_column_name_1: key_column_value_1, ..n} All key fields for the object
- #
- # @raise [V2::SBFMalformedRequestError] if one of the required params is not passed in
- # @raise [V2::SBFBadRequestError] if one of the params passed in is not good
- #
- # @return [Hash] A hash of the data for that object
- #
- def self.get(key_field_hash)
- # Find any combination of input keys which represent a unique entry
- key_columns = entity_class.key_columns(*key_field_hash.keys)
- raise V2::SBFInternalServerError if key_columns.nil?
- # Select only the key fields - in case the user gave us extra
- key_values = key_columns.map { |k| key_field_hash[k] }
- entity_class.get(*key_values)
- end
- def self.find(filter = nil, order = nil, limit = nil, offset = nil, total = false)
- # Start a blank select * query
- query = entity_class.all
- # Parse order and apply (if an order was given)
- unless order.empty?
- parsed_order = parse_order(order)
- query = query.all(parsed_order)
- end
- # Apply filter
- unless filter.empty?
- # Back up the links because recursively applying the filter is going to wipe them out. :(
- links = Set.new(query.instance_variable_get('@query').instance_variable_get('@links').to_a)
- # Parse the filter
- parsed_filter = parse_filter(filter)
- # Filter will be an array of nested arrays and/or hashes. Arrays serve to define parenthetical expressions.
- # Hashes serve to define query parameters.
- query = recursively_apply_filter(parsed_filter, query)
- # Need to replace the original links b/c they are not preserved.
- query_instance = query.instance_variable_get('@query')
- if query_instance
- links.merge(query_instance.instance_variable_get('@links').to_a)
- query_instance.instance_variable_set('@links', links.to_a)
- end
- end
- # Add the limit and offset if they are both set
- #
- # NOTE: You can't specify ONLY an offset
- if !limit.empty? && !offset.empty?
- query = query.all(limit: limit, offset: offset)
- # Add the limit if it is set
- elsif !limit.empty?
- query = query.all(limit: limit)
- end
- # Calculate total also, if requested
- if total
- # Just return the current length if it is < than the limit (since there can't be any more)
- return [query, query.length] if limit.nil? || query.length < limit && offset.to_i <= 0
- # Force a count distinct.
- parsed_filter = [{}] if parsed_filter.empty?
- recursively_add_unique_filter(parsed_filter)
- # Perform a count query using the same filter
- return [query, recursively_apply_filter(parsed_filter, entity_class.all).count]
- end
- query
- end
- # Recursive to find the first hash and add a 'unique' boolean key
- #
- def self.recursively_add_unique_filter(filter)
- if filter.is_a?(Hash)
- filter[:unique] = true
- return true
- elsif filter.is_a?(Array)
- filter.each do |item|
- return true if recursively_add_unique_filter(item)
- end
- end
- end
- # Calls list with the given filter but returns the first entity or nil.
- #
- def self.find_first(filter, order = nil)
- find(filter, order, 1).first
- end
- # This method joins queries and subqueries in a recursive fashion and returns the result
- #
- def self.recursively_apply_filter(filters, base_query)
- return base_query if filters.empty?
- query = nil
- condition = nil
- (0..filters.length).step(2) {|index|
- filter = filters[index]
- if filter.is_a?(Hash)
- # If the filter is a has we can apply it immediately
- filtered_query = base_query.all(filter)
- elsif filter.is_a?(Array)
- # If the filter is an array we need to call this method with it
- filtered_query = recursively_apply_filter(filter, base_query)
- else
- # Must have a properly formatted filter
- LOG.info { "Invalid filter entry #{filter} in filter #{filters}" }
- raise V2::SBFBadRequestError, errors: 'filter'
- end
- if index == 0
- # If this is our first entry, set the query to it without joining with any other conditional
- query = filtered_query
- else
- # All other even entries will need to be joined with the conditional
- condition = filters[index - 1]
- unless entity_class.valid_condition?(condition)
- LOG.info { "Invalid filter condition #{condition} in filter #{filters}" }
- raise V2::SBFBadRequestError,
- errors: 'filter'
- end
- query = query.send(condition, filtered_query)
- end
- }
- query
- end
- private_class_method :recursively_apply_filter
- def self.parse_filter(input_filter)
- # Return if input filter is nil or empty
- return nil if input_filter.empty?
- # Outermost part of the filter should be an array
- input_filter = [input_filter] if input_filter.is_a?(Hash)
- raise(V2::SBFBadRequestError, errors: 'filter') unless input_filter.is_a?(Array)
- # Loop over the filter array and parse. If we encounter an array, assume it is
- # a nested filter that needs to be recursively parsed
- input_filter.map do |filter_entry|
- if filter_entry.is_a?(Hash)
- {}.tap do |filter_hash|
- recursively_parse_filter_operands(filter_entry) do |operand, value|
- # If we specify multiple filter_hash operands which are identical,
- # put in an array so an 'in' query is performed
- existing_value = filter_hash[operand]
- if existing_value
- if existing_value.is_a?(Array)
- # value could be a single thing or an array so we have to be careful to add all of the things
- existing_value.push(*Array(value))
- else
- # value could be a single thing or an array so we have to be careful to add all of the things
- filter_hash[operand] = Array(existing_value).push(*Array(value))
- end
- else
- filter_hash[operand] = value
- end
- end
- end
- elsif filter_entry.is_a?(Array)
- # Entry is a sub-filter. Recursively parse it.
- parse_filter(filter_entry)
- elsif entity_class.valid_condition?(filter_entry.to_sym)
- # Entry is a condition. Add it back unmodified
- filter_entry.to_sym
- else
- LOG.info { "Invalid filter entry #{filter_entry} in filter #{input_filter}" }
- raise V2::SBFBadRequestError, errors: 'filter'
- end
- end
- end
- private_class_method :parse_filter
- # rubocop:disable MethodLength, CyclomaticComplexity
- def self.recursively_parse_filter_operands(entry, current_model = entity_class, operands = [], &block)
- entry.each do |key, value|
- # If the value is not an enumerable, there is no more parsing to do.
- #
- # Also, if the value is an array and none of it's values are enumerable,
- # then we must be specifying a range, so we are also done.
- if !value.is_a?(Enumerable) || (value.is_a?(Array) && !value.empty? && !value.any? { |it| it.is_a?(Enumerable) })
- # If the key is an operator, combine the operands and create the query operator object.
- if current_model.operators.include?(key)
- # :eql and :in are assumed and it will error if you set them
- operand = operands.join('.').to_sym
- operand = operand.send(key) unless key == :eql || key == :in
- prop_name = operands.last.to_sym
- # Otherwise, the key must be a column name. Add it as an operand.
- elsif current_model.valid_column?(key)
- operand = operands.empty? ? key.to_sym : "#{operands.join('.')}.#{key}".to_sym
- prop_name = key.to_sym
- else
- LOG.info { "Invalid filter key #{key}" }
- raise V2::SBFBadRequestError, errors: 'filter'
- end
- # Parse values to their appropriate types. This is mainly to facilitate with date searches.
- property_class = current_model.properties.values_at(prop_name).first
- if property_class.methods.include?(:typecast_to_primitive) && !value.is_a?(Enumerable)
- value = property_class.send(:typecast_to_primitive, value)
- end
- yield [operand, value]
- # This is a weird edge case to handle the way roles are transformed by the view layer.
- # I really can't think of a more elegant solution.
- #
- # Handles a case like: {roles [{id: 1}, {id: 2}]}
- elsif value.is_a?(Array) && value.all? { |it| it.is_a?(Enumerable) }
- sub_model = current_model.association_model(key)
- sub_operand = operands.dup << key
- value.each do |it|
- recursively_parse_filter_operands(it, sub_model, sub_operand, &block)
- end
- # If the key is an association, make a recursive call with the updated values for current model and operand
- elsif current_model.valid_association?(key)
- sub_model = current_model.association_model(key)
- sub_operand = operands.dup << key
- recursively_parse_filter_operands(value, sub_model, sub_operand, &block)
- # If we have gotten here and the key is a column, that must mean that we have a complex value. Use recursion to parse it.
- # Handles cases like {id: {gt: 1, lt: 100}}
- elsif current_model.valid_column?(key)
- sub_operand = operands.dup << key
- recursively_parse_filter_operands(value, current_model, sub_operand, &block)
- else
- LOG.info { "Invalid filter entry #{key}, #{value}" }
- raise V2::SBFBadRequestError, errors: 'filter'
- end
- end
- end
- # rubocop:enable MethodLength, CyclomaticComplexity
- private_class_method :recursively_parse_filter_operands
- def self.parse_order(input_order)
- # Return if input order is nil or empty
- return nil if input_order.empty?
- # Order should be a Hash
- raise(V2::SBFBadRequestError, errors: 'order') unless input_order.is_a?(Hash)
- # Start with a blank query object
- query = entity_class.all.query
- # Create the order array
- orders = Set.new
- links = Set.new
- # Recursively iterate through the input and
- recursively_parse_order_operands(input_order) do |order, link|
- orders << order
- links |= link
- end
- query.instance_variable_set('@order', orders.to_a)
- query.instance_variable_set('@links', links.to_a)
- # return the query order object
- query
- end
- private_class_method :parse_order
- def self.recursively_parse_order_operands(orders, current_model = entity_class, links = [], &block)
- orders.each do |key, value|
- # If value is not enumerable, then we are done recursing
- if !value.is_a?(Enumerable)
- # Make sure the key is a valid column
- if current_model.valid_column?(key)
- operand = current_model.properties.values_at(key).first
- else
- LOG.info { "Invalid order column #{key}" }
- raise V2::SBFBadRequestError, errors: 'order'
- end
- # Make sure the value is a valid direction
- value = value.to_sym
- if current_model.orders.include?(value)
- yield [DataMapper::Query::Direction.new(operand, value), links]
- else
- LOG.info { "Invalid order direction #{value}" }
- raise V2::SBFBadRequestError, errors: 'order'
- end
- elsif current_model.valid_association?(key)
- sub_model = current_model.association_model(key)
- sub_links = links.dup << current_model.relationships[key.to_s].inverse
- recursively_parse_order_operands(value, sub_model, sub_links, &block)
- else
- LOG.info { "Invalid order key #{key}" }
- raise V2::SBFBadRequestError, errors: 'order'
- end
- end
- end
- private_class_method :recursively_parse_order_operands
- def self.aggregate(filter, aggregate)
- # Start a blank select * query
- query = entity_class.all
- # Apply filter
- unless filter.empty?
- # Parse the filter
- parsed_filter = parse_filter(filter)
- # Filter will be an array of nested arrays and/or hashes. Arrays serve to define parenthetical expressions.
- # Hashes serve to define query parameters.
- query = recursively_apply_filter(parsed_filter, query)
- end
- # Format aggregates appropriately
- raise V2::SBFMalformedRequestError, 'At least one aggregation must be specified' if aggregate.empty?
- parsed_aggregate = aggregate.map { |k, v|
- v = v.to_sym if v.is_a?(String)
- raise V2::SBFMalformedRequestError, "Invalid aggregate #{v}" unless entity_class.valid_aggregate?(v)
- k.send(v)
- }
- # Perform aggregate query
- result = query.aggregate(*parsed_aggregate)
- # Normalize the results
- result = [result] unless result.is_a?(Array)
- # Loop over results and map then back to their column name, returning the result
- {}.tap do |hsh|
- result.each_with_index do |it, index|
- hsh[parsed_aggregate[index].target] = it
- end
- end
- end
- # Updates an existing object using the input data
- #
- # @option entity [Object] Entity object to be updated
- # @option data [Hash] Hash of parameters to be updated
- #
- # @raise [V2::SBFMalformedRequestError] if one of the required params is not passed in
- # @raise [V2::SBFBadRequestError] if one of the params passed in is not good
- # @raise [V2::SBFSaveError] if the existing object is not found
- #
- def self.update(entity, data = {}, skip_deduplicator = false)
- # Prune out relationship information
- parent_data, child_data = prune_parents_and_children(data)
- # Make sure the params are valid
- ensure_valid_update_params(data)
- # Create the relationships and relate them to the base entity. Then save the entity (all in one transaction)
- entity_class.transaction do
- # Create entities for parent relationships
- prepare_parent_relationships(entity, parent_data)
- # Set attributes to what was specified
- entity.attributes = data unless data.empty?
- # Ensure the policy allows tenant to update the entity
- authorize_update(entity, default_tenant)
- # Call generic save method
- skip_deduplicator ? V2::ProfileService.save(entity, skip_deduplicator) : save(entity)
- # Create child relationships
- prepare_child_relationships(entity, child_data)
- # Ensure the policy allows tenant to update the entity
- authorize_update(entity, default_tenant)
- # Call save again in case the setting of relationship made the entity dirty
- skip_deduplicator ? V2::ProfileService.save(entity, skip_deduplicator) : save(entity)
- end
- end
- # Errors if an invalid column is found
- #
- def self.ensure_valid_update_params(params)
- # Don't allow a user to set any serial fields
- params.reject! { |k, _| entity_class.serial?(k) } # Throw error?
- # We need to allow for required parameters which have a default value configured
- required_properties_without_defaults = entity_class.requireds - entity_class.columns_with_default_values
- # If a required parameter is being updated it must not be null
- check_params_set_if_present(required_properties_without_defaults, params)
- # Make sure that all parameters are valid columns
- check_params_valid(entity_class.columns, params)
- end
- private_class_method :ensure_valid_update_params
- # Deletes an object
- #
- # @raise [V2::SBFBadRequestError] if there is a problem destroying object
- # @raise [V2::SBFError] if there is a general error
- # @raise [V2::SBFDeleteError] if deleting the object fails
- #
- # @return [Array] The object that was just deleted
- #
- def self.delete(entity)
- begin
- # Ensure the policy allows tenant to destroy the entity
- authorize_destroy(entity, default_tenant)
- unless entity.destroy
- error_string = entity.collect_errors.join('. ')
- if error_string.empty?
- error_string = 'Delete failed'
- entity.send(:relationships).each do |relationship|
- next unless relationship.class.name.include?('OneToMany') || relationship.class.name.include?('OneToOne')
- next unless relationship.respond_to?(:enforce_destroy_constraint)
- error_string << ", related #{relationship.name}" unless relationship.enforce_destroy_constraint(entity)
- end
- end
- # TODO: May need to also iterate over any relationships we are changing...
- raise V2::SBFBadRequestError, "Could not delete #{entity.class.name.demodulize}: #{error_string}"
- end
- nil
- rescue V2::SBFError => sbfe
- raise sbfe
- rescue => e
- LOG.error { "Error deleting database entity: #{e}\n#{e.backtrace[0..10].join("\n")}" }
- raise V2::SBFDeleteError, "Could not delete #{entity.class.name.demodulize}"
- end
- # Return the entity
- entity
- end
- def self.do_delete_procedure(entity, input_fields = nil)
- # Ensure the policy allows tenant to destroy the entity
- authorize_destroy(entity, default_tenant)
- # Manually kick off the before destroy hooks for the entity
- entity.send(:execute_hooks_for, :before, :destroy)
- begin
- # Allow for a user to specify the input fields - if they don't, default to the keys for the entity
- input_fields = (entity.class.keys + entity.class.pseudo_keys.to_a) if input_fields.nil?
- input_fields = Array(input_fields)
- # Grab fields and current user as agruments.
- arguments = input_fields.map { |key_field| entity.send(key_field) }
- arguments << RequestSession[:profile_id].to_i
- # Stored procedure is probably as follows:
- name = "uspDelete#{entity.class.name.demodulize}"
- # Execute the procedure
- execution_status = do_procedure(name, arguments)
- unless execution_status == 0
- LOG.error { "Stored procedure returned #{execution_status} which was interpreted as a failure." }
- raise V2::SBFDeleteError, "Could not delete #{entity.class.name.demodulize}"
- end
- # Manually kick off the before destroy hooks for the entity
- entity.send(:execute_hooks_for, :after, :destroy)
- rescue V2::SBFError => sbfe
- raise sbfe
- rescue => e
- LOG.error { "Error calling stored procedure #{name} with arguments #{arguments}: #{e}\n#{e.backtrace[0..10].join("\n")}" }
- raise V2::SBFDeleteError, "Could not delete #{entity.class.name.demodulize}"
- end
- entity
- end
- def self.do_procedure(name, arguments)
- host = SETTINGS['db_writeonly_hostname']
- database = SETTINGS['db_writeonly_name']
- user = SETTINGS['db_writeonly_username']
- password = SETTINGS['db_writeonly_password']
- client = Mysql2::Client.new(
- host: host,
- database: database,
- username: user,
- password: password,
- flags: Mysql2::Client::MULTI_STATEMENTS
- )
- # utf encode the input and escape any potentially dangerous characters.
- arguments.map! { |argument| escape(client, argument) }
- # Build the stored procedure call
- command = "call #{name}('#{arguments.join("','")}');"
- LOG.debug { "Calling stored procedure as [ #{command} ]." }
- result = client.query(command)
- # Clear all remaining data off the socket
- client.store_result while client.next_result
- # First entry in result should contain a hash of the execution status
- result.first['ExecutionStatus']
- ensure
- client.close if client
- end
- def self.escape(db, value)
- return value unless value.is_a? String
- db.escape(utf_encode(value))
- end
- private_class_method :escape
- # Takes in an array of relationship names and verifies that the value of all key fields in the passed in data matches
- # the current value of those keys on the entity. This should ensure that only updates are happening to those objects
- # and not any creates
- def self.disallow_relationship_move(entity, data, relationship_names)
- Array(relationship_names).each do |relationship_name|
- next unless data.key?(relationship_name)
- passed_in_relationship = data[relationship_name]
- relationship = entity.class.relationships.entries.select { |r| r.name == relationship_name }[0]
- relationship.parent_key.to_ary.each_with_index do |prop, index|
- if passed_in_relationship[prop.name] != entity.send(relationship.child_key.to_ary[index].name)
- raise V2::SBFBadRequestError, "Changing #{relationship_name}s is not allowed via this route"
- end
- end
- end
- end
- ##############################################
- ################ DEPRECATED ##################
- # USE THE NEW METHODS WHICH DISTINGUISH #
- # BETWEEN PARENTS AND CHILDREN #
- ##############################################
- # This method removes the relationship data from the original data and stores it in it's own hash
- def self.prune_relationship_data(data)
- {}.tap do |hsh|
- entity_class.associations.each do |k, _|
- hsh[k] = data.delete(k) if data.key?(k)
- end
- end
- end
- private_class_method :prune_relationship_data
- # This method should create or update the inter-relationships for the entity
- #
- def self.prepare_entity_relationships(entity, params)
- prepare_parent_relationships(entity, params)
- end
- private_class_method :prepare_entity_relationships
- ##############################################
- ##############################################
- ##############################################
- # This method removes the relationship data from the original data and stores it in it's own hash
- def self.prune_parents_and_children(data)
- parents = {}
- children = {}
- entity_class.associations.each do |k, _|
- next unless data.key?(k)
- if entity_class.many_to_one_associations.include?(k)
- parents[k] = data.delete(k)
- else
- children[k] = data.delete(k)
- end
- end
- [parents, children]
- end
- private_class_method :prune_relationship_data
- # This method should create or update the inter-relationships for the entity
- #
- def self.prepare_parent_relationships(entity, params)
- params.each do |k, v|
- # We want to continue if we have no changes, but check for enumerable class to handle the case
- # where we want to set the relationship to nil
- next if v.is_a?(Enumerable) && v.empty?
- relationship_class = entity_class.associations[k]
- # If the value is an array, then the relationship is a collection and we need
- # call create_or_update for each item in the collection
- if v.is_a?(Array)
- value = v.map { |it| create_or_update(relationship_class, it) }
- else
- value = create_or_update(relationship_class, v)
- end
- entity.send("#{k}=", value)
- end
- end
- private_class_method :prepare_parent_relationships
- # This method should create or update the inter-relationships for the entity
- #
- def self.prepare_child_relationships(entity, params)
- params.each do |k, v|
- # We want to continue if we have no changes, but check for enumerable class to handle the case
- # where we want to set the relationship to nil
- next if v.is_a?(Hash) && v.empty?
- relationship_class = entity_class.associations[k]
- # We need to pull the parent fields from the entity and place them in the data to create
- parent_key_values = parent_entity_key_field_values(entity, k)
- # If the value is an array, then the relationship is a collection and we need
- # call create_or_update for each item in the collection
- if v.is_a?(Array)
- value = v.map { |it| create_or_update(relationship_class, it.merge(parent_key_values)) }
- elsif v.respond_to?(:merge)
- value = create_or_update(relationship_class, v.merge(parent_key_values))
- end
- entity.send("#{k}=", value)
- end
- end
- private_class_method :prepare_child_relationships
- # If the hash contains the key fields for the given entity class, this method will perform a get
- # using those key fields and then perform an update of the entity using the input data.
- #
- # If the hash does not contain all primary key fields, then this method will attempt to do a create
- # for the given entity class using the input data
- #
- # Finally, this method returns the updated/created entity
- #
- def self.create_or_update(relationship_class, data)
- # We only care if data has been passed in
- return nil if data.empty?
- # Dynamically figure out the service class
- entity_service_class = const_get(name.deconstantize).const_get("#{relationship_class.to_s.demodulize}Service")
- # Perform create if key fields are not specified
- if entity_service_class.needs_create?(data, relationship_class)
- LOG.debug { "Creating new #{relationship_class}" }
- return entity_service_class.create(data)
- end
- # Probably an update, then. Look up the entity using the key fields
- LOG.debug { "Looking up #{entity_service_class} using #{data}" }
- entity = entity_service_class.get(data)
- # So I'm not sure if this is what we want or not. The problem is that for several entities the keys are derived
- # from other tables and there is no auto-generated id field. This means there is no real way to tell if
- # the user was trying to update or create the entity. I believe it makes sense to assume if the user passed in
- # information that has key fields (PLUS additional data) but does not exist, then that means we want to create it.
- if entity.nil?
- # If the entity has serial fields (auto-increment) or they passed in the exact key fields (and there are other fields on the entity),
- # assume they were attempting to just link the entities and do not try to do a create
- if !relationship_class.serials.empty? ||
- ((data.keys - relationship_class.keys).empty? && !(relationship_class.columns - relationship_class.keys).empty?)
- raise V2::SBFBadRequestError, "#{relationship_class.to_s.demodulize} #{data.values.join(', ')} does not exist"
- end
- LOG.debug { "Creating new #{relationship_class}" }
- return entity_service_class.create(data)
- end
- # Update entity using the passed in data
- entity_service_class.update(entity, data)
- end
- private_class_method :create_or_update
- # Loop over the parent keys, get the values of the properties they represent,
- # then create a hash of them using they child key names which they map to
- def self.parent_entity_key_field_values(entity, relationship_name)
- {}.tap do |hsh|
- relationship = entity_class.relationships.find { |it| it.name == relationship_name }
- # TODO: So this is a work around for the time being. I'm not 100% sure of exactly what needs to
- # happen if this is a M2M relationship b/c you have to take the through/via layer into account.
- # I believe this can be accomplished using the "links" method but I don't need it at the moment
- # and so I'm going to just leave this work-around for now until we have a better idea of what is needed.
- next if relationship.is_a? DataMapper::Associations::ManyToMany::Relationship
- parent_keys = relationship.parent_key.to_ary
- child_keys = relationship.child_key.to_ary
- parent_keys.each_with_index do |parent_property, index|
- child_property = child_keys[index]
- value = entity.send(parent_property.name)
- hsh[child_property.name] = value unless value.nil?
- end
- LOG.debug { "Updating #{entity.class.name} entity with #{hsh}" }
- end
- end
- private_class_method :parent_entity_key_field_values
- def self.authorize_create(entity, tenant = default_tenant)
- unless V2::SecurityHelper.__security_bypass__
- if V2::Policy.defined_for?(entity)
- unless V2::Policy.find(entity, tenant).create_allowed?
- values = entity.keys.map { |it| "#{it}: #{entity.send(it) || '<not created>'}" }
- raise V2::SBFUnauthorizedError, "Unauthorized to create #{entity.class.name.demodulize} (#{values.join(', ')})"
- end
- end
- end
- end
- private_class_method :authorize_create
- def self.authorize_update(entity, tenant = default_tenant)
- unless V2::SecurityHelper.__security_bypass__
- if V2::Policy.defined_for?(entity)
- unless V2::Policy.find(entity, tenant).update_allowed?
- values = entity.keys.map { |it| "#{it}: #{entity.send(it)}" }
- raise V2::SBFUnauthorizedError, "Unauthorized to update #{entity.class.name.demodulize} (#{values.join(', ')})"
- end
- end
- end
- end
- private_class_method :authorize_update
- def self.authorize_destroy(entity, tenant = default_tenant)
- unless V2::SecurityHelper.__security_bypass__
- if V2::Policy.defined_for?(entity)
- unless V2::Policy.find(entity, tenant).destroy_allowed?
- values = entity.keys.map { |it| "#{it}: #{entity.send(it)}" }
- raise V2::SBFUnauthorizedError, "Unauthorized to destroy #{entity.class.name.demodulize} (#{values.join(', ')})"
- end
- end
- end
- end
- private_class_method :authorize_destroy
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement