app/models/maestrano/connector/rails/concerns/connec_helper.rb
module Maestrano::Connector::Rails::Concerns::ConnecHelper
extend ActiveSupport::Concern
module ClassMethods
def dependancies
# Meant to be overloaded if needed
{
connec: '1.0',
impac: '1.0',
maestrano_hub: '1.0'
}
end
def get_client(organization)
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", headers: {'Accept' => 'application/json'})
response = JSON.parse(response.body)
@@connec_version = response['ci_branch'].delete('v')
end
@@connec_version
end
def connec_version_lt?(version, organization)
version = Gem::Version.new(version)
current_version = Gem::Version.new(connec_version(organization))
current_version < version
rescue
true
end
# 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)
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 }
connec_id = unfolded_connec_entity['id'].find { |id| id['provider'] == 'connec' }['id']
unfolded_connec_entity['id'] = id_hash ? id_hash['id'] : nil
# 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
# 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
# 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?
# Follow embedment path, remplace if it's not an array or a hash
case field
when Array
field.each do |f|
fold_references_helper(f, array_of_refs.dup, organization)
end
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
if array_of_refs.empty? && field
return entity.delete(ref) if field.is_a?(String) # ~retro-compatibility to ease transition aroud Connec! idmaps rework. Should be removed eventually.
id_hash = field.find { |id| id[:provider] == organization.oauth_provider && id[:realm] == organization.oauth_uid }
if id_hash
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
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
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