ITENTIAL ADAPTERS TECHNICAL RESOURCE
Adapter Code
Adapter Code Files
adapter.js
This file contains the adapter code that is exposed to the rest of the Itenital Automation Platform (IAP). Public methods in this file can be called from other parts of IAP so that actions from the adapter can be provided to the other system. The Adapter Builder makes a method in this file for each action within the API. This file also extends the adapterBase.js file so that it has access to all the adapterBase methods, and the adapter run time libraries. It is assumed that you will modify the methods in this file as needed to make use of essential capabilities and add any needed logic.
adapterBase.js
This file contains generic adapter methods. It should not be modified because when an adapter is migrated, this file is replaced with a newer version. This file includes the connect and healthcheck methods, which also should not be modified. If there is a need to modify these methods, you can override them in the adapter.js file.
Runtime Libraries
Most of the adapter code lives here. The Runtime Libraries provide all the essential capabilities, including translation, filtering, authentication, throttling, retries, redirects, mock data results, etc.
What’s in adapter.js?
Generic Methods
- constructor: This method is commented out because the method in adapterBase.js is usually sufficient. However, if you need to add properties specific to the adapter you can uncomment this method and follow the comments.
- healthCheck: This method is also in adapterBase.js however, there are times you might want to add the information that is passed into the healthcheck so this method has been added to provide that capability. It should always call super.healthCheck to run the healthcheck.
- hasEntity: This method exists to provide additional capabilities to the Itential Automation Platform (IAP). It will tell IAP if the adapter has the desired entity.
- verifyCapability: This method exists to provide additional capabilities to IAP. It will tell IAP if the adapter has the desired entity and if an action is provided, it can tell IAP if the action is available on that entity. If using this method, it must be updated for all the entities in the adapter as the Adapter Builder does not currently update this method.
- updateEntityCache: This method is used to update the adapter cache. If using entity caching, this method must be updated for all the entities in the adapter as the Adapter Builder does not currently update this method.
ADAPTER.JS
constructor(prongid, properties) {
// Instantiate the AdapterBase super class
super(prongid, properties);
// Uncomment if you have things to add to the constructor like using your own properties.
// Otherwise the constructor in the adapterBase will be used.
// Capture my own properties -they need to be defined in propertiesSchema.json
if (this.allProps && this.allProps.myownproperty) {
mypropvariable = this.allProps.myownproperty;
}
}
healthCheck(reqObj, callback) {
hasEntity(entityType, entityId, callback) {
verifyCapability(entityType, actionType, entityId, callback) {
updateEntityCache() {
Adapter Specific Methods
Part 1: Parameters & Logging
The first part of the specific method includes:
- The method parameters used when sending a request. These parameters will vary depending on the data that needs to be sent to the other system. There can be 0 to n parameters.
- Setting the origin for all trace logging and error handling.
ADAPTER.JS
sampleAPIcall(param1, param2, ..., callback) {
const meth = 'adapter- sampleAPIcall’;
const origin = `${this.id}-${meth}`;
log.trace(origin);
Part 2: Data Validation
- When there is an error in data validation, the adapter utilizes the formatErrorObject library call to make sure the error is formatted consistently. This includes:
- Information on the origin of the error.
- A key in the error.json to provide additional information for the error. You can add errors to the error.json and use the keys when defining the error to get more information. If the key does not match any errors in the error.json, the key will be used as a display string for the error.
- An array of variables to populate the display message.
- The error code that came from the other system.
- The raw response that came from the other system.
- The stack trace or exception.
ADAPTER.JS
/* HERE IS WHERE YOU VALIDATE DATA */
if (param1 === undefined || param1 === null || param1 === '') {
const errorObj = this.requestHandlerInst.formatErrorObject(this.id, meth, 'Missing Data', ['param1'], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
Part 3: Construct the Request Object
- The third part of the API methods includes setting the request object.
- The example below shows the detail that can go into the request object that is sent to the adapter-utils.
ADAPTER.JS
/* HERE IS WHERE YOU SET THE DATA TO PASS INTO REQUEST */
..
// There will be lines here to modify the parameters you can hardcode information or take them in from parameters and format them accordingly.
..
// set up the request object - payload, uriPathVars, uriQuery, uriOptions, addlHeaders, authData, callProperties,
// filter, priority and event
const reqObj= {
payload: bodyToSendOnCall, // This is formatted based on the datatype
uriPathVars: pathVariablesArray, // This is an array of path variables
uriQuery: queryParams, // This is a JSON object
uriOptions: optionParams, // This is a JSON object
addlHeaders: callSpecificAdditionalHeaders, // This is a JSON object
authData: dynamicAuthenticationBodyData, // This is a JSON object
callProperties: dynamicCallProps, // This is a JSON object – match property structure
filter: dynamicResponseFilter, // This is a JSONQuery string
priority: #, // This is a number that corresponds to value of a defined priority
event: eventname // This is an event to trigger when data comes back
};
Part 4: Call to Adapter Libraries
- The last part of the API methods includes the call to the adapter libraries.
- The entity must match the name of the entity directory.
- The action must match the name of the action in the action.json file.
- The returnDataFlag tells the adapter libraries whether to send back data or return a success/fail flag.
- You can customize what happens when errors occur as well as alter the data that is returned from the adapter.
- Remember: Translation is less work if you define the schema and allow the adapter libraries to handle it. There may be cases, however, where you need to modify the data being returned.
ADAPTER.JS
// Make the call -
// identifyRequest(entity, action, requestObj, returnDataFlag, callback)
return this.requestHandlerInst.identifyRequest(‘entity’, ‘action', reqObj, true, (irReturnData, irReturnError) => {
// if we received an error or there is no response on the results
// return an error
if (irReturnError) {
/* HERE IS WHERE YOU CAN ALTER THE ERROR MESSAGE */
return callback(null, irReturnError);
}
if (!Object.hasOwnProperty.call(irReturnData, 'response')) {
const errorObj = this.requestHandlerInst.formatErrorObject(this.id, meth, 'Invalid Response’, [‘action’], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
/* HERE IS WHERE YOU CAN ALTER THE RETURN DATA */
// return the response
return callback(irReturnData, null);
});
What’s In adapterBase.js?
Generic Methods
- constructor: Sets up the information that is needed for the adapter (properties) and instantiates the adapter libraries.
- refreshProperties: Provides the ability to update the adapter properties without having to bounce the adapter.
- connect: Starts up the adapter and sets the healthcheck based on the properties.
- healthcheck: Runs the call defined in the .system entity, which is a healthcheck action to the other system. It then reports the status of the adapter’s ability to talk to that system.
- getAllFunctions: Gets a list of all the functions in the adapter.
- getWorkflowFunctions: Gets a list of all functions that should be workflow functions. This method excludes the generic adapter functions.
- checkActionFiles: Checks the action files against the actionSchema to validate that no errors exist.
- getQueue: Returns the throttling queue.
- encryptProperty: Used to encrypt properties that are used by the adapter. The adapter can decrypt properties when it needs to use them.
- addEntityCache: If using a cache for entities, this method will add the entities into the cache. Entity caching can be used to improve efficiency on some requests.
- entityInList: Will determine if the entity is in the list of entities.
- capabilityResults: Prepares proper results for verifyCapability.
- getAllCapabilities: Returns all the capabilities (API calls) that the adapter supports.
ADAPTER.JS
constructor(prongid, properties) {
refreshProperties(properties) {
connect() {
healthCheck(reqObj, callback) {
getAllFunctions() {
getWorkflowFunctions() {
checkActionFiles() {
getQueue(callback) {
encryptProperty(property, technique, callback) {
addEntityCache(entityType, entities, key, callback) {
entityInList(entityId, data) {
capabilityResults(results, callback) {
getAllCapabilities() {
Modifications
- Remember, do not modify the adapterBase.js file. It is a file the Adapter Builder owns, and when the adapter is updated, the adapterBase.js will be replaced with a newer version. Therefore any changes you make will be overwritten.
- If you need to make modifications to a specific call (e.g. connect), you can override the call in the adapter.js file.
- If you find a bug in adapterBase.js, or you want to add another capability, open a ticket for the Itential Adapter Team and we will make the requested modifications and then migrate the fix to all adapters.
Runtime Libraries
Common Uses
- The two most common uses of the Runtime Libraries are:
- Making a request to an external system.
- Formatting an error object to make it consistent with all other errors.
- A third call is being added to return metrics information on the adapter.
ADAPTER.JS
// Make the call -
// identifyRequest(entity, action, requestObj, returnDataFlag, callback)
return this.requestHandlerInst.identifyRequest(‘entity’, ‘action', reqObj, true, (irReturnData, irReturnError) => {
// if we received an error or there is no response on the results
// return an error
if (irReturnError) {
/* HERE IS WHERE YOU CAN ALTER THE ERROR MESSAGE */
return callback(null, irReturnError);
}
if (!Object.hasOwnProperty.call(irReturnData, 'response')) {
const errorObj = this.requestHandlerInst.formatErrorObject(this.id, meth, 'Invalid Response’, [‘action’], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
/* HERE IS WHERE YOU CAN ALTER THE RETURN DATA */
// return the response
return callback(irReturnData, null);
});
Purpose
- The Runtime Libraries allow fast construction of adapters and provide various capabilities, which are generally consistent across all adapters.
- Through the Runtime Libraries, these capabilities are available instantaneously for all adapters:
- Authentication
- Throttling
- Translation
- Consistent Error Handling
- Metrics
- Filtering
- Encoding
- Encryption
- Intelligent Handling
Capabilities
- Authentication: Systems authenticate in different ways. The adapter libraries are frequently extended with different authentication methods to support complex authentication practices.
- Throttling: Throttling enables successful integration with systems that cannot handle heavy loads or have system licenses that limit concurrent usage.
- Translation: Translation of JSON objects occur to enable successful transactions between IAP and the external system. Additionally, the adapter libraries handles JSON->XML and XML->JSON so that communication with systems that speak XML can be accomplished without having to do any additional work.
- Consistent Error Handling: The adapter libraries generate consistent error messages for any error an adapter receives.
- Metrics: Metrics are gathered and saved for calls to determine the outcome of various actions in an adapter over time. These metrics include:
- Round trip time
- Capability time (measure of time in the adapter libraries)
- Overall time (if the adapter.js calls the adapter libraries method)
- Response handling (# of different status codes)
- Filtering: When the external system does not have its own filtering capabilities, the adapter’s Runtime Libraries can filter data that is specific to your needs and remove any unwanted information that should not be passed into IAP.
- Encoding/Encryption: When data needs to be encoded or encrypted before it is sent to another system, the adapter’s Runtime Libraries can be set-up to do this automatically prior to the data being sent out. In addition, when the data is received, it will be decoded or decrypted prior to being returned.
- Intelligent Handling: This capability handles all proxies or redirects, and any automatic retries when an error or timeout has occurred. In addition, for token-based systems, if the token has expired, the adapter will know to pull a new token and retry the request. The Runtime Libraries also provide the ability to run the adapter in standalone mode (stub mode) by returning mock data that exists within the adapter rather than integrating to another system. This improves the ability to test the adapter as well as build workflows in IAP when the other system is not available to integrate with.
Modifications
- The Runtime Libraries is opensource and it is possible to contribute to it . Since the libraries is utilized by most adapters, all modifications are subject to review and regression testing.
- If you find a bug in the adapter libraries:
- Create a ticket (ISD, IPSO or ADAPT).
- You can work the ticket or allow the Itential Adapter Team to fix the bug.
- Once all changes are made, update the adapter-utils dependency in your adapter or run the adapterMigrator.
- To update your adapter-utils dependency:
- Change the version in the package.json dependencies.
- rm –rf node modules
- rm package-lock.json
- npm install
Customizations & Examples
Dynamic Input
- You can change any information on the request in adapter.js when setting the request object.
- Information can be placed into adapter.js if it applies to a specific action.
- Dynamic information can come from IAP if it is in the method signature of the adapter method.
ADAPTER.JS
someCall(id, body, auditValue, callback) {
/* HERE IS WHERE YOU SET THE DATA TO PASS INTO REQUEST */
..
let callHeaders = { hdr1: auditValue };
..
// set up the request object - payload, uriPathVars, uriQuery, uriOptions, addlHeaders, authData, callProperties,
// filter, priority and event
const reqObj = {
payload: body,
uriPathVars: [ id ],
uriOptions: { page: 1 },
authData: callHeaders
};
Request Payload
- Payload is simply the body that is sent out on the request to the other system.
- If the payload is JSON, it will be translated based on the schema for the particular action. The reason it is translated is when IAP refers to a field as X and the other system refers to it as Y, the adapter needs to put the value in X into Y prior to calling the other system so that the other system will understand the request.
- The payload can also be plain text or xml in which case nothing will done with it.
- Other actions can be done based on the requestDatatype defined in the action.json file. These include encoding, encrypting, URL handling as a form, and translating from JSON to XML.
ADAPTER.JS
const reqObj = {
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=doggie]',
priority: 1,
event: 'giveMeMyData'
};
Request Path Variables
- Path Variables are used to dynamically set data in the call path to the other system.
- When the call path is defined (see path example to the right), the path variables will be used to replace {pathv1} and {pathv2} in the call path.
- uriPathVars is an array
- uriPathVars[0] will be used to replace {pathv1} and uriPathVars[1] will be used to replace {pathv2}
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=doggie]',
priority: 1,
event: 'giveMeMyData'
};
api/v1/devices/{pathv1}/interface/{pathv2}?{query}"
Replaced With
api/v1/devices/dev123/interface/int123?{query}"
Request Query Params
- Query parameters are translated based on the defined schema for the particular action. The reason they are translated is when IAP refers to a field as X and the other system refers to it as Y, the adapter needs to put the value in X into Y prior to putting it into the query parameters for the call to the other system. This will allow the other system to understand the request.
- The query object is then converted to a querystring by the Adapter libraries and appended to the call.
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=doggie]',
priority: 1,
event: 'giveMeMyData'
};
"/api/v1/devices/{pathv1}/interface/{pathv2}?{query}"
is replaced with
"/api/v1/devices/grp123/interface/dev123?name=anyname"
Request Options
- Call options are query parameters that are not translated. These are generally metadata on the request. Some examples include page or page_size.
- The call option object is converted to a querystring by the Adapter libraries and appended to the call.
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=doggie]',
priority: 1,
event: 'giveMeMyData'
};
"/api/v1/devices/{pathv1}/interface/{pathv2}?{query}"
is replaced with
"/api/v1/devices/grp123/interface/dev123?name=anyname&page=2"
Dynamic Headers
- In some cases, dynamic headers should be added to a call. This can include changing the content-type, providing audit headers or supplying other headers needed on the request.
- Dynamic headers can be encoded in the adapter.js, or you can pass them into adapter.js and then add a method param.
- If headers are the same on every call and static, consider using a global_request property to define the headers instead of adding them into every method in the adapter.js.
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=doggie]',
priority: 1,
event: 'giveMeMyData'
};
Dynamic Authentication Data
- In some cases, dynamic authentication is needed based on a certain set of criteria (e.g. domains).
- Use authData to feed this information into the Runtime libraries so that a request can be dynamically authenticated based on the criteria that was set.
- When the authData param is added to adapter.js, it is added to the Authentication request object.
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=doggie]',
priority: 1,
event: 'giveMeMyData'
};
Individual Call Properties
- As shown previously, you can override adapter properties on an individual request. You only need to get the info into the adapter by exposing parameters in the adapter.js method.
- Most properties can be changed on a request. Exceptions to this include throttling (do not bypass or change the queue handling) and healthcheck.
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=doggie]',
priority: 1,
event: 'giveMeMyData'
};
Filtering Response
- Not all systems provide filtering capabilities.
- The adapter will filter the response data based on dynamic criteria provided in the request.
- The filter string is a JSON Query used to filter the incoming data. Since the filter is in the request object it can be set-up on a request-by-request basis.
- This param is used to request that a system return all the devices that start with “ABC”.
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=abc]',
priority: 1,
event: 'giveMeMyData'
};
Priority and Event
- Priority and event are both specific to throttling.
- Priority is a way to prioritize items in the throttle queue.
- The priority must correspond to the value defined in the priorities array in the adapter properties.
- The adapter will queue the request based on the percent value for the matching priority. Zero percent (0%) indicates the request is at the front of the queue and 100% means at the end of the queue.
- Event tells the adapter how to return the result when the caller has decided not to wait for the response.
- The adapter will trigger the event when the response is ready so that the caller can then know the result is ready.
ADAPTER.JS
const reqObj =
payload: { garbage: 'need since post' },
uriPathVars: [groupId, deviceId],
uriQuery: { name: 'anyname' },
uriOptions: { page: 2 },
addlHeaders: { audit: 'turnOn' },
authData: { domain: 'abc' },
callProperties: {
stub: true,
request: {
attempt_timeout = 60000
}
},
filter: '[*name=abc]',
priority: 1,
event: 'giveMeMyData'
};
Customizing the Response
- You can change any information in the response in adapter.js prior to sending it back to IAP. This can include:
- Handling embedded errors that may not be detected by the generic adapter libraries.
- Performing calculations on the data that is returned and adding the results into the response.
- There is no need to do translations here if schemas are set up properly, but you can if desired.
ADAPTER.JS
// Make the call -
// identifyRequest(entity, action, requestObj, returnDataFlag, callback)
return this.requestHandlerInst.identifyRequest(‘entity’, ‘action', reqObj, true, (irReturnData, irReturnError) => {
// if we received an error or there is no response on the results
// return an error
if (irReturnError) {
/* HERE IS WHERE YOU CAN ALTER THE ERROR MESSAGE */
return callback(null, irReturnError);
}
if (!Object.hasOwnProperty.call(irReturnData, 'response')) {
const errorObj = this.requestHandlerInst.formatErrorObject(this.id, meth, 'Invalid Response’, [‘action’], null, null, null);
log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
return callback(null, errorObj);
}
/* HERE IS WHERE YOU CAN ALTER THE RETURN DATA */
// return the response
const newObj = {
number: irReturnData.response.length,
data: irReturnData.response
}
return callback(newObj, null);
});
Get Started with Itential
Start a 30 day free trial, or contact us to discuss your goals and how we can help.