app/models/maestrano/connector/rails/concerns/connec_helper.rb in maestrano-connector-rails-1.2.3 vs app/models/maestrano/connector/rails/concerns/connec_helper.rb in maestrano-connector-rails-1.3.0

- old
+ new

@@ -15,53 +15,76 @@ client = Maestrano::Connec::Client[organization.tenant].new(organization.uid) client.class.headers('CONNEC-EXTERNAL-IDS' => 'true') client end + # Returns a string of the tenant's current connec version. + # Can use Gem::Version for version comparison def connec_version(organization) @@connec_version = Rails.cache.fetch("connec_version_#{organization.tenant}", namespace: 'maestrano', expires_in: 1.day) do response = get_client(organization).class.get("#{Maestrano[organization.tenant].param('connec.host')}/version") response = JSON.parse(response.body) @@connec_version = response['ci_branch'] end @@connec_version end - # Replace the ids arrays by the external id - # If a reference has no id for this oauth_provider and oauth_uid but has one for connec, returns nil + # Replaces the arrays of id received from Connec! by the id of the external application + # Returns a hash {entity: {}, connec_id: '', id_refs_only_connec_entity: {}} + # If an array has no id for this oauth_provider and oauth_uid but has one for connec, it returns a nil entity (skip the record) def unfold_references(connec_entity, references, organization) - unfolded_connec_entity = connec_entity.with_indifferent_access + references = format_references(references) + unfolded_connec_entity = connec_entity.deep_dup.with_indifferent_access not_nil = true # Id id_hash = unfolded_connec_entity['id'].find { |id| id['provider'] == organization.oauth_provider && id['realm'] == organization.oauth_uid } - unfolded_connec_entity[:__connec_id] = unfolded_connec_entity['id'].find { |id| id['provider'] == 'connec' }['id'] + connec_id = unfolded_connec_entity['id'].find { |id| id['provider'] == 'connec' }['id'] unfolded_connec_entity['id'] = id_hash ? id_hash['id'] : nil - # Other refs - references.each do |reference| - not_nil &&= unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization) + # Other references + # Record references are references to other records (organization_id, item_id, ...) + references[:record_references].each do |reference| + not_nil &= unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization) end - not_nil ? unfolded_connec_entity : nil + # Id references are references to sub entities ids (invoice lines id, ...) + # We do not return nil if we're missing an id reference + references[:id_references].each do |reference| + unfold_references_helper(unfolded_connec_entity, reference.split('/'), organization) + end + unfolded_connec_entity = not_nil ? unfolded_connec_entity : nil + + # Filter the connec entity to keep only the id_references fields (in order to save some memory) + # Give an empty hash if there's nothing left + id_refs_only_connec_entity = filter_connec_entity_for_id_refs(connec_entity, references[:id_references]) + + {entity: unfolded_connec_entity, connec_id: connec_id, id_refs_only_connec_entity: id_refs_only_connec_entity} end + # Replaces ids from the external application by arrays containing them def fold_references(mapped_external_entity, references, organization) + references = format_references(references) mapped_external_entity = mapped_external_entity.with_indifferent_access - (references + ['id']).each do |reference| + + # Use both record_references and id_references + the id + (references.values.flatten + ['id']).each do |reference| fold_references_helper(mapped_external_entity, reference.split('/'), organization) end + mapped_external_entity end + # Builds an id_hash from the id and organization def id_hash(id, organization) { id: id, provider: organization.oauth_provider, realm: organization.oauth_uid } end + # Recursive method for folding references def fold_references_helper(entity, array_of_refs, organization) ref = array_of_refs.shift field = entity[ref] return if field.blank? @@ -69,18 +92,19 @@ case field when Array field.each do |f| fold_references_helper(f, array_of_refs.dup, organization) end - when HashWithIndifferentAccess + when Hash fold_references_helper(entity[ref], array_of_refs, organization) else id = field entity[ref] = [id_hash(id, organization)] end end + # Recursive method for unfolding references def unfold_references_helper(entity, array_of_refs, organization) ref = array_of_refs.shift field = entity[ref] # Unfold the id @@ -91,25 +115,139 @@ entity[ref] = id_hash['id'] elsif field.find { |id| id[:provider] == 'connec' } # Should always be true as ids will always contain a connec id # We may enqueue a fetch on the endpoint of the missing association, followed by a re-fetch on this one. # However it's expected to be an edge case, so for now we rely on the fact that the webhooks should be relativly in order. # Worst case it'll be done on following sync + entity.delete(ref) return nil end + true # Follow embedment path else - unless field.blank? - case field - when Array - field.each do |f| - unfold_references_helper(f, array_of_refs.dup, organization) - end - when HashWithIndifferentAccess - unfold_references_helper(entity[ref], array_of_refs, organization) + return true if field.blank? + case field + when Array + bool = true + field.each do |f| + bool &= unfold_references_helper(f, array_of_refs.dup, organization) end + bool + when Hash + unfold_references_helper(entity[ref], array_of_refs, organization) end end - true + end + + # Transforms the references into an hash {record_references: [], id_references: []} + # References can either be an array (only record references), or a hash + def format_references(references) + return {record_references: references, id_references: []} if references.is_a?(Array) + references[:record_references] ||= [] + references[:id_references] ||= [] + references + end + + # Returns the connec_entity without all the fields that are not id_references + def filter_connec_entity_for_id_refs(connec_entity, id_references) + return {} if id_references.empty? + + entity = connec_entity.dup.with_indifferent_access + tree = build_id_references_tree(id_references) + + filter_connec_entity_for_id_refs_helper(entity, tree) + + # TODO, improve performance by returning an empty hash if all the id_references have their id in the connec hash + # We should still return all of them if at least one is missing as we are relying on the id + entity + end + + # Recursive method for filtering connec entities + def filter_connec_entity_for_id_refs_helper(entity_hash, tree) + return if tree.empty? + entity_hash.slice!(*tree.keys) + + tree.each do |key, children| + case entity_hash[key] + when Array + entity_hash[key].each do |hash| + filter_connec_entity_for_id_refs_helper(hash, children) + end + when Hash + filter_connec_entity_for_id_refs_helper(entity_hash[key], children) + end + end + end + + # Builds a tree from an array of id_references + # input: %w(lines/id lines/linked/id linked/id) + # output: {"lines"=>{"id"=>{}, "linked"=>{"id"=>{}}}, "linked"=>{"id"=>{}}} + def build_id_references_tree(id_references) + tree = {} + + id_references.each do |id_reference| + array_of_refs = id_reference.split('/') + + t = tree + array_of_refs.each do |ref| + t[ref] ||= {} + t = t[ref] + end + end + + tree + end + + # Merges the id arrays from two hashes while keeping only the id_references fields + def merge_id_hashes(dist, src, id_references) + dist = dist.with_indifferent_access + src = src.with_indifferent_access + + id_references.each do |id_reference| + array_of_refs = id_reference.split('/') + + merge_id_hashes_helper(dist, array_of_refs, src) + end + + dist + end + + # Recursive helper for merging id hashes + def merge_id_hashes_helper(hash, array_of_refs, src, path = []) + ref = array_of_refs.shift + field = hash[ref] + + if array_of_refs.empty? && field + value = value_from_hash(src, path + [ref]) + if value.is_a?(Array) + hash[ref] = (field + value).uniq + else + hash.delete(ref) + end + else + case field + when Array + field.each_with_index do |f, index| + merge_id_hashes_helper(f, array_of_refs.dup, src, path + [ref, index]) + end + when Hash + merge_id_hashes_helper(field, array_of_refs, src, path + [ref]) + end + end + end + + # Returns the value from a hash following the given path + # Path sould be an array like [:lines, 0, :id] + def value_from_hash(hash, path) + value = hash + + begin + path.each do |p| + value = value[p] + end + value + rescue NoMethodError + nil + end end end end