require 'rest-client'
require 'uri'
require 'net/http'
require 'rubygems'
require 'json'
require 'yaml'
require 'cgi'
class HTTPErrorException < StandardError
attr_reader :CODE, :MESSAGE
def initialize(code, message)
super("HTTP Error #{code}: #{message}")
@CODE = code.to_i
@MESSAGE = message
end
end
# Connector for Pipeliner's REST calls. For more information see:
# https://workspace.pipelinersales.com/community/api/
#
# The MIT License (MIT)
#
# Copyright (c) 2014-2018 Pipelinersales, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author :
# Pipelinersales Inc.
#
# Version :
# 1.8.0
class PipelinerRestAPI
attr_reader :FLAG_ROLLBACK_ON_ERROR, :FLAG_IGNORE_ON_ERROR, :FLAG_INSERT_OR_UPDATE
attr_reader :FLAG_IGNORE_AND_RETURN_ERRORS, :FLAG_VALIDATE_ONLY_UPDATED_FIELDS
attr_reader :FLAG_IGNORE_READONLY, :FLAG_USE_VALIDATION_LEVEL
attr_reader :VALIDATION_SKIP_UNCHANGED, :VALIDATION_SKIP_FORM_CUSTOM_FIELD, :VALIDATION_SKIP_RECALCULATIONS, :VALIDATION_SKIP_KPI
VERSION = '1.8.0'
USER_AGENT = "Pipeliner_Ruby_API_Client/#{VERSION}"
FLAG_ROLLBACK_ON_ERROR = 0
FLAG_IGNORE_ON_ERROR = 1
FLAG_INSERT_OR_UPDATE = 2
FLAG_IGNORE_AND_RETURN_ERRORS = 8
FLAG_VALIDATE_ONLY_UPDATED_FIELDS = 256
FLAG_IGNORE_READONLY = 2**9
FLAG_USE_VALIDATION_LEVEL = 16777216
VALIDATION_SKIP_UNCHANGED = 3
VALIDATION_SKIP_FORM_CUSTOM_FIELD = 5
VALIDATION_SKIP_RECALCULATIONS = 9
VALIDATION_SKIP_KPI = 17
SKIP_UTF8MB4_CHARS = 33
# Creates a new RestAPI connector which provides all REST calls as methods.
#
# Parameters
# ----------
# serviceUrl : string
# Service URL can be obtained in API Access section from Customers Portal.
def initialize(serviceUrl)
@serviceUrl = "#{serviceUrl}/rest_services/v1/"
end
# Sets username and password for connection.
#
# username : string
# API Token obtained from API Access in customers portal.
# password : string
# API Password obtained from API Access in customers portal.
def setCredentials(username, password)
@username = username
@password = password
end
# Returns current revision in server database.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# Returns
# -------
# retVal : string
# Newest revision in server database.
#
# Examples
# --------
# retVal = service.getCurrentRevision(teamPipelineID)
# puts retVal
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getCurrentRevision(teamPipeline)
response = request("#{@serviceUrl}#{teamPipeline}/TaskTypes?limit=1&revision=true")
response.headers[:revision]
end
# Returns list of fields for specific entity.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# entityName : string
# Name of entity class or entity collection.
# params : string, hash
# Params for requested entity. It could be string identical with HTTP params followed after '?' or dict. See example.
#
# Returns
# -------
# retVal : Hash
# JSONArray of JSONObjects which contains information about fields:
# API_NAME - field name used in API as key.
# CALC_FORMULA - calculated formula for autocalculated fields, or None if not set.
# CHOICES - if dropdown or radio button, then dictionary is set with IDs of datasets as keys and their names as values, otherwise None is set.
# CUSTOM - true if custom field, false if system field.
# DEFAULT - default value for field. If not set, then None.
# DESCRIPTION - description of field.
# ID - unique identificator of field. Used as ID_FIELD in Data entity.
# MAX_VALUE - max allowed value. For decimal types, it is maximal number, for unicode types it is maximal length of string.
# MIN_VALUE - min allowed value for decimal an datetime types.
# NAME - field name shown in Pipeliner.
# PL_TYPE - Pipeliner field type.
# READONLY - true if field is readonly, otherwise false.
# REQUIRED - true if field is required, otherwise false.
# TYPE - Type of field. Supported types - [string, unicode, datetime, long, Decimal, list]
#
# Examples
# --------
# retVal = service.getFields(teamPipelineID, "Contacts")
# puts retVal[0]["API_NAME"]
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getFields(teamPipeline, entityName, params="")
JSON::parse(miscMethod("#{@serviceUrl}#{teamPipeline}/getFields/#{entityName}#{safeQuery(params)}"))
end
# Retrieves list of entities in Hash.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# entity : string
# Name of object.
# params : string, hash
# Params for requested entity. It could be string identical with HTTP params followed after '?' or dict. See example.
#
# Returns
# -------
# retVal : Hash
# Hash table with response body.
#
# Examples
# --------
# result = service.getEntities(teamPipelineID, "Contacts", {"limit" => 5, "filter" => {"FIRST_NAME": "Todd"}})
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getEntities(teamPipeline, entity, params="")
result = miscMethod("#{@serviceUrl}#{teamPipeline}/#{entity}#{safeQuery(params)}")
JSON::parse(result)
end
# Retrieves list of entities in Hash.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# entity : string
# Name of object.
# params : Hash
# Hash which contains parameters for filtering.
#
# Returns
# -------
# retVal : Hash
# Hash table with response body.
#
# Examples
# --------
# result = service.searchEntities(teamPipelineID, "Contacts", {"limit" => 5, "filter" => {"terms" => {"FIRST_NAME": "Todd"}}})
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def searchEntities(teamPipeline, entity, params=nil)
params = {} if params.nil?
params = params.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
response = sendPOSTRequest("#{@serviceUrl}#{teamPipeline}/search/#{entity}", params)
result = {
"Result" => JSON::parse(response.body),
"COUNT" => getItemsCount(response)
}
if params.key?(:revision)
result["REVISION"] = getRevision(response)
end
result
end
# Creates a new entity in collection. If ID is provided in data, then update is performed for existing entity.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# entity : string
# Name of object.
# data : Hash
# Hash with created or updated fields.
# revision : string
# If set, then the record will be validated against server side current revision.
# validation_level : integer
# More specific validation parameters can be modified by combination of flags:
# SKIP_UNCHANGED[3] - This will skip validation for all unchanged fields.
# SKIP_FORM_CUSTOM_FIELD[5] - Validations specified in form settings will be skipped.
# SKIP_RECALCULATION[9] - Recalculations for auto-calculated fields will be skipped as well as calculated validations.
# SKIP_KPI[17] - KPI for insights will not be created.
# SKIP_UTF8MB4_CHARS[33] - When an utf8mb4 characters were used, then the error will not be raised and characters will be removed instead.
#
# Returns
# -------
# retVal : string
# ID of created or updated entity.
#
# Examples
# --------
# data = {"ID" => "PY-7FFFFFFF-12345678-1234-1234-1234-1234567890", "FIRST_NAME" => "Todd"}
# result = service.setEntity(teamPipelineID, "Contacts", data)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def setEntity(teamPipeline, entity, data, revision=nil, validation_level=nil)
params = []
revision = revision.nil? ? "" : "revision=#{revision}"
validation_level = validation_level.nil? ? "" : "validation_level=#{validation_level}"
params << revision unless revision.empty?
params << validation_level unless revision.empty?
params = params.join('&')
params = '?' + params unless params.empty?
getLocation(sendPOSTRequest("#{@serviceUrl}#{teamPipeline}/#{entity}#{params}", data))
end
# Creates a new entities. If ID is provided in data, then update is performed for existing entity.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# entity : string
# Name of object.
# data : Hash Array
# Hash array with created or updated fields.
# flag : integer
# The processing of command can be modified by combination of flags:
# FLAG_ROLLBACK_ON_ERROR[0] – if any error is occurred no entity will be processed ,entire batch will be rollbacked and exception thrown.
# FLAG_IGNORE_ON_ERROR[1] – it any error occures for an entity , this entity is ignored and system continues with the next one.
# FLAG_GET_NO_DELETED_ID[4] - The method returns list of ID, which cannot be deleted. Could be used with combination with FLAG_IGNORE_ON_ERROR.
# FLAG_IGNORE_AND_RETURN_ERRORS[8] – if any error occurs for an entity, this entity is ignored and system continues with the next one.
# FLAG_VALIDATE_ONLY_UPDATED_FIELDS[256] - flag will validate only updated fields in entity instead of all fields.
# FLAG_IGNORE_READONLY[512] - setting this flag, read-only custom fields can be overridden through server API.
# revision : string
# If set, then records will be validated against server side current revision.
# validation_level : integer
# More specific validation parameters can be modified by combination of flags:
# SKIP_UNCHANGED[3] - This will skip validation for all unchanged fields.
# SKIP_FORM_CUSTOM_FIELD[5] - Validations specified in form settings will be skipped.
# SKIP_RECALCULATION[9] - Recalculations for auto-calculated fields will be skipped as well as calculated validations.
# SKIP_KPI[17] - KPI for insights will not be created.
# SKIP_UTF8MB4_CHARS[33] - When an utf8mb4 characters were used, then the error will not be raised and characters will be removed instead.
#
# Returns
# -------
# retVal : array
# Array of results for requested entities.
#
# Examples
# --------
# data = [{"ID" => "PY-7FFFFFFF-12345678-1234-1234-1234-1234567890", "FIRST_NAME" => "Todd"}]
# result = service.setEntities(teamPipelineID, "Contact", data)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def setEntities(teamPipeline, entity, data, flag=0, revision=nil, validation_level=nil)
revision = revision.nil? ? "" : "&revision=#{revision}"
validation_level = validation_level.nil? ? "" : "&validation_level=#{validation_level}"
JSON::parse(sendPOSTRequest("#{@serviceUrl}#{teamPipeline}/setEntities?entityName=#{entity}&flag=#{flag}#{revision}#{validation_level}", data).body).map do |item|
if item.is_a? Hash
"serverApiError ##{item['errorcode']}: #{item['message']}"
else
item
end
end
end
# Deletes entity with given primary key.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# entity : string
# Name of object.
# id : string
# The unique identificator of entity.
#
# Returns
# -------
# retVal : string
# ID of deleted entity.
#
# Examples
# --------
# entity_id = "PY-7FFFFFFF-12345678-1234-1234-1234-1234567890"
# result = service.deleteEntity(teamPipelineID, "Contacts", entity_id)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def deleteEntity(teamPipeline, entity, id)
req = RestClient::Request.new(
method: :delete,
url: URI.escape("#{@serviceUrl}#{teamPipeline}/#{entity}/#{id}"),
user: @username,
password: @password,
headers: {'User-Agent' => USER_AGENT, 'Content-Type' => 'application/json'}
)
safeResponse(req, ["204"])
return id
end
# Deletes entities with given primary key.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
# entity : string
# Name of object.
# data : array
# Array with IDs to delete.
# flag : integer
# The processing of command can be modified by combination of flags:
# FLAG_ROLLBACK_ON_ERROR[0] – if any error is occurred no entity will be processed ,entire batch will be rollbacked and exception thrown.
# FLAG_IGNORE_ON_ERROR[1] – it any error occures for an entity , this entity is ignored and system continues with the next one.
# FLAG_GET_NO_DELETED_ID[4] - The method returns list of ID, which cannot be deleted. Could be used with combination with FLAG_IGNORE_ON_ERROR.
# FLAG_IGNORE_AND_RETURN_ERRORS[8] – if any error occurs for an entity, this entity is ignored and system continues with the next one.
#
# Returns
# -------
# retVal : array
# Array of result for deleted entities.
#
# Examples
# --------
# entity_ids = ["PY-7FFFFFFF-12345678-1234-1234-1234-1234567890"]
# result = service.deleteEntities(teamPipelineID, "Contact", entity_ids)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def deleteEntities(teamPipeline, entity, data, flag=0)
JSON::parse(sendPOSTRequest("#{@serviceUrl}#{teamPipeline}/deleteEntities?entityName=#{entity}&flag=#{flag}", data).body)
end
# Retrieves list of collections in the pipeline.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
#
# Returns
# -------
# retVal : array
# Array of collections.
#
# Examples
# --------
# result = service.getCollections(teamPipelineID)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getCollections(teamPipeline)
retVal = miscMethod("#{@serviceUrl}#{teamPipeline}")
parseArray(retVal)
end
# Retrieves the datacenter URL for team space.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
#
# Returns
# -------
# retVal : string
# Datacenter URL for current team space.
#
# Examples
# --------
# result = service.getTeamPipelineUrl(teamPipelineID)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getTeamPipelineUrl(teamPipeline)
miscMethod("#{@serviceUrl}#{teamPipeline}/teamPipelineUrl")[1..-2]
end
# Retrieves the current server's UTC datetime for queried team space.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
#
# Returns
# -------
# retVal : string
# Current server's UTC date as string.
#
# Examples
# --------
# result = service.getServerAPIUtcDateTime(teamPipelineID)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getServerAPIUtcDateTime(teamPipeline)
miscMethod("#{@serviceUrl}#{teamPipeline}/serverAPIUtcDateTime")[1..-2]
end
# Retrieves the hash of API error codes.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
#
# Returns
# -------
# retVal : Hash
# Hash of API error codes.
#
# Examples
# --------
# result = service.getErrorCodes(teamPipelineID)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getErrorCodes(teamPipeline)
json = JSON::parse(miscMethod("#{@serviceUrl}#{teamPipeline}/errorCodes"))
json.each { |key, value| json[key] = value.strip }
end
# Retrieves hash of public entities.
#
# Parameters
# ----------
# teamPipeline : string
# Name of team space to be used.
#
# Returns
# -------
# retVal : hash
# Hash of collection names as keys and entity names as values.
#
# Examples
# --------
# result = service.getEntityPublic(teamPipelineID)
# puts result
#
# Raises
# ------
# HTTPErrorException :
# Raises on HTTP error code from server with API error message.
def getEntityPublic(teamPipeline)
json = JSON::parse(miscMethod("#{@serviceUrl}#{teamPipeline}/entityPublic"))
json.each { |key, value| json[key] = value.strip }
end
#############################################################################
private
@@NO_CONNECTION_MESSAGE = "The remote name could not be resolved: "
@@NO_CONNECTION = 408
def safeResponse(req, codes)
begin
response = req.execute()
rescue SocketError
raise HTTPErrorException.new(@@NO_CONNECTION, @@NO_CONNECTION_MESSAGE + req.url)
rescue RestClient::BadRequest, RestClient::Unauthorized => ex
response = ex.response
end
code = response.code
raise HTTPErrorException.new(code, getErrorMessage(response.body)) unless codes.include?(code.to_s)
response
end
def parseArray(strArray)
YAML::load(strArray.gsub(/(\,)(\S)/, "\\1 \\2"))
end
def request(url)
req = RestClient::Request.new(
method: :get,
url: URI.escape(url),
user: @username,
password: @password,
headers: {'User-Agent' => USER_AGENT}
)
safeResponse(req, ["200"])
end
def miscMethod(url)
request(url).body
end
def getLocation(response)
response.headers[:location].split("/")[-1]
end
def getRevision(response)
response.headers[:revision]
end
def getItemsCount(response)
res = /items \d+-\d+\/(\d+)/.match(response.headers[:content_range])
res = res.captures()[0].to_i unless res.nil?
res
end
def safeValue(value)
if value.instance_of?(Array)
value.map { |x| safeValue(x) }
else
if value.instance_of?(String)
['\\', ':', '|'].each do |special|
if value.include?(special)
if special != '\\'
value = value.gsub(special, "\\#{special}")
else
value = value.gsub(special, "\\\\#{special}")
end
end
end
end
value.to_s
end
end
def safeQuery(params)
if not params or params == ""
""
elsif params.instance_of?(String)
"?#{params}"
else
query = []
params.each do |key, value|
if ['offset', 'limit', 'after', 'entityname'].include? key.to_s.downcase
query << "#{key}=#{safeValue(value)}"
elsif ['loadonly', 'sort'].include? key.to_s.downcase
query << "#{key}=#{safeValue(value).join('|')}"
elsif ['filter'].include? key.to_s.downcase
convertedValue = value.map do |k, v|
if v.instance_of?(Array)
v[0] = safeValue(v[0]).join(':') if v.include? 'in'
"#{k}::#{v.join('::')}"
else
"#{k}::#{safeValue(v)}::eq"
end
end
query << "#{key}=#{convertedValue.join('|')}"
end
end
"?#{query.join('&')}"
end
end
def sendPOSTRequest(url, data)
req = RestClient::Request.new(
method: :post,
url: URI.escape(url),
user: @username,
password: @password,
headers: {'User-Agent' => USER_AGENT, 'Content-Type' => 'application/json'},
payload: data.to_json
)
safeResponse(req, ["200", "201"])
end
def getErrorMessage(message)
begin
json = JSON.parse(message)
json['message']
rescue JSON::ParserError => e
message
end
end
end