Integration with the external application

Now that you're all setup with Maestrano, it's time to take a look at the external application you are integrating with. Hopefully it has one or several gems for both the authentication process and the API calls. Understanding the API you integrate with is very important, so you need to have a read through their documentation.

You will probably have to request API keys and adds them to the application.yml alongside the Maestrano's ones.


Authentication

You need to implement an authentication process to get a token and be able to do API calls on behalf of the user.

The connector engine is thought to be able to use oauth authentication processes, and an oauth_controller.rb is provided as an example. Please note that it is only an example and you will have to implements most of it, as well as create the needed routes, and use them in the provided view.

  For more detailed information, visit: Authentication strategies

If all went well, you should now be able to use the 'Link this company to...' link on the home page. Congrats!

Preparing synchronizations

The aim of the connector is to perform synchronizations between Connec!™ and the external application, meaning fetching data on both ends, process them, and push the result to the relevant parties. The Connec!™ part and the synchronization process itself is handled by the framework, so all you have to do is to implement some methods to bridge the calls to the external application.

External.rb

The first file to look for is models/maestrano/connector/rails/external.rb. It contains three methods that you need to implement:

  • external_name, which is used for logging purpose only, and should only return the name of the external application, e.g.

    def self.external_name
      'This awesome CRM'
    end
  • get_client, which should return either the external application gem api client, or, in the worst case, a custom API client. e.g

    See the section below for a general overview and guidelines

    def self.get_client(organization)
      MyAppAPIManager.new(organization)
    end
  • entities_list that defines the list of all the entities synchronized by the connector

    def self.entities_list
      %w(item person payment order)
    end

Entity.rb

The second file is models/maestrano/connector/rails/entity.rb. It contains some methods to retrieve and send data to the external application API, as well as some helpers methods.

The details of each methods are explained in the entity.rb file provided, and you can have a look at the example section.

FAQ

 I don't have update timestamps on some of my entities, what should I do?

If creation timestamps are available they can be used as a fallback. In the worst case scenario use Time.now

 I don't have any creation timestamps in my app, what should I do?
If update timestamps are available they can be used as a fallback. In the worst case scenario use Time.now
 The API does not expose timestamps

If the API does not allow filtering on timestamps or does not expose them, there is no way to filter only updated entities during a synchronization process. In this case, all entities will be re-imported which may impact performances.

Entities shared and naming conventions

When creating your files, some guidelines can be followed in order to keep consistency and avoid confusion.

 Files and Classes

When naming your ruby files, the convention is to use the Connec! endpoint name.

I.e. if you are mapping People (Connec!)  to Contacts (YourApplication) the ruby file will be named person.rb following Rails conventions.

Your class skeleton would then look like this:

class Entities::Person < Maestrano::Connector::Rails::Entity
  def self.connec_entity_name
    'Person'
  end

  def self.external_entity_name
    'Contact'
  end

  def self.mapper_class
    PersonMapper
  end
  
  # Rest of the code

end

GITHUB Please refer to Maestrano's open source repositories for more examples. Snippets are taken from the BaseCRM repository


If you need to implement complex entities in YourApplication, you can follow a similar process.

I.e. if you are mapping People and Organizations (Connec!)  to Contacts (YourApplication) the ruby file for the complex entity would be named person_and_organization.rb.

class Entities::PersonAndOrganization < Maestrano::Connector::Rails::ComplexEntity
  def self.connec_entities_names
    %w(Person Organization)
  end

  def self.external_entities_names
    %w(Contact)
  end

  # Rest of the code
end


You will then have three sub-entities named person.rb , organization.rb and contact.rb respectively. The two mapper files for sub-entities will be named person_mapper.rb and organization_mapper.rb

class Entities::SubEntities::Person < Maestrano::Connector::Rails::SubEntityBase
  
  def self.external?
    false
  end

  def self.entity_name
    'Person'
  end

  # Rest of the code
end



class Entities::SubEntities::Organization < Maestrano::Connector::Rails::SubEntityBase
  
  def self.external?
    false
  end

  def self.entity_name
    'Organization'
  end

  # Rest of the code
end
class Entities::SubEntities::Contact < Maestrano::Connector::Rails::SubEntityBase

  def self.external?
    true
  end

  def self.entity_name
    'Contact'
  end

  def self.mapper_classes
    {
      'Person' => Entities::SubEntities::PersonMapper,
      'Organization' => Entities::SubEntities::OrganizationMapper
    }
  end

  def self.references
    {
      'Person' => Entities::SubEntities::PersonMapper.person_references,
      'Organization' => Entities::SubEntities::OrganizationMapper.organization_references
    }
  end

  def self.object_name_from_external_entity_hash(entity)
    if entity['is_organization']
      entity['name']
    else
      "#{entity['first_name']} #{entity['last_name']}"
    end
  end
end

API Client

If YourApp does not provide a Gem to query the API you would need to create your own.

GUIDELINES

 Pagination

Most APIs support pagination options. The following examples assume you will be using RestClient [https://github.com/rest-client/rest-client]


The typical and easiest way to implement a pagination option in your client is possible when the response provided by the API implements '[prev_page]' and '[next_page'] fields:

entities = results_of_your_first_call

  while your_saved_response['next_page'] # or ['some_nested_field']['next_page']
    response = RestClient.get "#{your_saved_response['next_page']}", your_headers
    entities.concat YourDataParser.parse(response.body) # JSON.parse or equivalent if the API returns json
    raise 'No response received while fetching subsequent page' unless response && !response.body.blank?
    break if last_synchronization_date && entities.last['updated_at'] < last_synchronization_date
    your_saved_response = JSON.parse(response)['the_field_containing_next_page'] # The loop will break when '[next_page]' is nil  
 end

entities  


If YourApp API does not provide meta fields for pagination or uses a different standard you will have to implement a similar logic:

    # Assuming a first successefull response and data model: 
          {
            "Pagination": {
              "NumberOfItems": 14,
              "PageSize": 200,
              "PageNumber": 1,
              "NumberOfPages": 1
             },
            "Items": [
             {1},
             {2}
            ]
          }
     # Also assuming the API call to get a specific page is: 
       {YourAppUrl}/{entity_name}/{page_number}

    data = JSON.parse(response.body)

    pagination = data['Pagination']
    entities = data['Items']

    if pagination
      while pagination['NumberOfPages'] > pagination['PageNumber']
        next_page = pagination['PageNumber'] + 1
        response = RestClient.get("base_url/#{entity_name}/#{next_page}", query)

        data = JSON.parse(response.body)
        pagination = data['Pagination']
        entities.concat data['Items']
      end
    end

entities 
 Batched calls

The Connector Framework supports two options that enable batched calls:

            __skip     and     __limit

They can be used to implement specific queries:

batched_call = opts[:__skip] && opts[:__limit]

# You can assign the values based on YourApp API standards (i.e. top/skip)
query_params = {top: opts[:limit], skip: opts[:skip] }

response = RestClient.get "https://base_url/#{entity_name}?#{query_params.to_query}", headers_get


It is possible to set the batch_size for the first synchronization in settings.yml

first_sync_batch_size: 25
 Error handling

Two custom classes are available at the moment to handle specific errors:

Maestrano::Connector::Rails::Exceptions::EntityNotFoundError
# and 
Maestrano::Connector::Rails::Exceptions::ExternalAPIError


Use ::EntityNotFoundError if the external API does not provide web-hooks for deletion and entities are not set to INACTIVE (or similar) but deleted entirely
An attempted update on a non existent entity will have to raise this specific error. I.e.:

rescue => e
  if e.class == RestClient::ResourceNotFound
    raise Maestrano::Connector::Rails::Exceptions::EntityNotFoundError.new "The record has been deleted in MyApp"
  else
    Maestrano::Connector::Rails::ConnectorLogger.log('warn', @organization, "Error sending entities", {external_entity_name: external_entity_name, external_id: external_id, message: e.message, backtrace: e.backtrace})
  end

Use ::ExternalAPIError for all the errors returned by the external API.

 Token refresh

When implementing the OAuth WebServer flow, you can use this simple implementation to make sure your token is refreshed when needed:


# In your API client

def refresh_token(organization)
  # the logic here would be ideally extracted to another Object/Service
  body = {
    "grant_type" => "refresh_token",
    "refresh_token" => "#{organization.refresh_token}"
  }
  headers = {
    "Content-Type" => "application/json",
    "Authorization" => "Basic #{ENV['your_api_key']}:#{ENV['your_client_secret']}}"
  }
  response = RestClient::Request.execute method: :post, url: "https://your_app_url/oauth2/token",
                                         payload: body.to_json, headers: headers
  parsed_response = JSON.parse(response)
  organization.update(
    oauth_token: parsed_response['access_token'],
    refresh_token: parsed_response['refresh_token']
  )
end

def with_token_refresh
  yield
rescue
  refresh_token(organization)
  yield
end
 


Now it is sufficient to wrap your API calls in a block:


def get_entities(entity_name, opts = {}, last_synchronization_date = nil)
  with_token_refresh do
    # Your API call and logic
  end
end
 
 Date filtering

In the connector framework last_synchronization_date is the parameter used to implement date filtering:

Your client should provide a way to limit the calls using the date provided and retrieve the entities that have been modified only after the specified date:

def get_entities(entity_name, opts = {}, last_synchronization_date = nil)
  with_token_refresh do
    # Usually, if last_synchronization_date is not nil you should use the API filtering capabilities (if provided) 
    # i.e. add to the query string "update_at=last_synchronization_date"

    # If the API does not support filtering by date, you might fetch the data ordered by updated_at(descending) and limit the number of records.
    # i.e. Break when entities.last['updated_at'] <= last_synchronization_date
  end
end