Quick Start: Developing Your First Adapter

Introduction

In this Quick Start, we're going to build a simple Output Adapter that receives shipment data and outputs a text file with the mode of transportation. This is an oversimplified use case, and your production adapter will probably do a lot more work, however the purpose of this adapter is to show you how to interact with Chain.io and register your adapter.

Business Requirements

Our adapter has the following requirements

  • Take a shipment as the input
  • Allow the user to configure a transformation table to adjust the mode of transportation if they want to
  • Give the user the option to fail or continue if the mode of transportation is not in our table
  • Return the file with the mode of transportation
  • Provide a user log message to the user letting them know what we did
  • Register this adapter so it's available as an option as a "Visibility - Receive Shipment Updates" output adapter

Adapter Sample Code

This sample code represents a plain javascript implementation of our webhook and callback. This could be built into an ExpressJS server or be implemented on a hosted platform like AWS Lambda.

const CryptoJS = require('crypto-js')

// Do NOT commit the shared secret into your code repository
// Use whatever secrets manager you feel is appropriate to your 
// development process to inject it into your code base.
const MY_CHAINIO_SECRET = process.env.CHAINIO_SHARED_SECRET

function makeSignature(body) {
	const hash = CryptoJS.HmacSHA256(body, MY_CHAINIO_SECRET)
	return CryptoJS.enc.Base64.stringify(hash)
}

function verifySignature(body, sig) {
  const expected = makeSignature(body)
  if (expected !== sig) {
    throw new Error('bad signature')
  }
}

async function processPayload (body, headers) {
  verifySignature(body, headers['x-chainio-signature']
  const userLogs = []
  let status = 'success'
  const { configuration, payload, callback } = JSON.parse(body)
  // Extract configuration values from request that the user
  // has configured in the Chain.io portal setup for the flow
  const { modeTable, errorOnMissing } = configuration
  // Extract the transport_mode property from the actual data payload
  // Each payload will be unique to the type of integration, this 
  // particular example is using a visibility register shipment 
  // payload (vis.register_shipment)
  const { transport_mode } = payload
  
  let returnPayload = transport_mode
  
  const conversion = modeTable.find(mt => mt.src === transport_mode)
  if (conversion) {
    returnPayload = conversion.dest
    userLogs.push({ 
      level: 'info', 
      message: `Converted ${transport_mode} to ${conversion.dest}`,
      timestamp: Date.now().toISOString()
    })
  } else if (errorOnMissing) {
    userLogs.push({ 
      level: 'error', 
      message: `Source mode ${transport_mode} was not found, processing stopped`,
      timestamp: Date.now().toISOString()
    })
    status = 'error'
  } else {
    userLogs.push({ 
      level: 'info', 
      message: `Source mode ${transport_mode} was not found in conversion table, leaving it alone`,
      timestamp: Date.now().toISOString()
    })
  }
  
  const callbackVal = {
    status,
    userLogs,
    callback // always return the callback property exactly as it was sent to you
    // This sample does not include the optional dataTags & files objects
    // you can learn more about them elsewhere in the documentation
  }
  
  if (status === 'success') {
    callbackVal.payload = [
      {
	      file_name: `mode-${Date.now()}.txt`,
        mime_type: 'text/plain',
  	    body: Buffer.from(returnPayload).toString('base64'),
        content_transfer_encoding: 'base64'
    	}
    ]
  }
  
  const body = JSON.stringify(callbackVal)
  
  return fetch('https://webhooks.chain.io/callback', {
    method: 'post',
    body,
    headers: {
      'Content-Type': 'application/json',
      'x-chainio-signature': makeSignature(body)
    } 
  })
}

Registering Your Adapter

In order for users to access your adapter, you'll need to register it using a developer key and our registration API.


// sample registration body 
const options = {
  method: 'POST',
  headers: {
    accept: 'application/json', 
    'content-type': 'application/json',
    'x-api-key': process.env.CHAINIO_API_KEY //secret API key - DO NOT STORE IN YOUR SOURCE CODE
  },
  body: JSON.stringify({
    integration_identifiers: ['vis.receive_shipment_update'],
    display_name: 'Mode Text File Generator',
    display_description: 'Make text file with your mode of transportation',
    support_url: 'https://mymodetranslator.com/help',
    adapter_type: 'output',
    webhook_url: 'https://fake.fake/my_url',
    shared_secret: 'abc1239193118$$$@!#1',
    adapter_icon_url: 'https://cdn-icons-png.flaticon.com/512/9908/9908191.png',
    publication_type: 'public',
    configuration_options: [
      {
        name: 'customerNumber',
        label: 'Customer Number',
        support_url: 'https://docs.co2-co.sample/help#customerNumber',
        description: 'Your CO2-Co customer number',
        config_type: 'regex',
        required: false,
        regex_config: {
          regex: '^[0-9]{5}$',
          regex_error: 'Your customer number must be exactly 5 digits.'
        }
      },
      {
        name: 'errorOnMissing',
        label: 'Error On Missing Mode',
        description: "Should this job fail if the mode isn't in the table or just pass along the value provided.",
        config_type: 'boolean'
      }
    ]
  })
};

fetch('https://webhooks.chain.io/adapters', options)
  .then(response => response.json())
  .then(response => console.log(response))
  .catch(err => console.error(err));