This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Custom Connectors

Learn about custom connectors, what they are and why they are used

What are Custom Connectors?

Kianda provides predefined datasource connectors such as the SQL server and SharePoint to allow you to connect your Kianda forms to particular data sources. A complete list of these predefined datasources is available at Data connectors. In addition to these predefined connectors, you can also create Custom connectors to enable you to configure your own connector to suit your needs.

Custom connectors work in the same way as other Kianda connectors, but with the added benefit of customisation.

What are Custom Connectors used for?

Custom connectors provide the infrastructure to allow developers to create a custom datasource, it provides hooks for pre- and post-processing of the query and the ability to customise the datasource settings for the connector.

Custom Connector VS Datasource

As previously mentioned, the Kianda platform provides a list of predefined datasource connectors, such as SharePoint, SQL server and SAP. These however cannot be customized for an individual companies needs.

Custom Connectors provide end-to-end customisation for datasource connections. The Custom Connector provides the architecture to allow customisation of pre- and post-processing of database queries.

For example when using a SharePoint data source you can use parameters for example selection of the environment and user credentials, but if you needed extra parameter, you could use the custom connector to provide this ability.

How to get started

There are three key steps that need to be implemented in order to create and use a customised connector as follows:

  1. Microservice - create a microservice that will implement metadata, query and test functions.

  2. Use Kianda features to create and test your customised connector - use Developer to register a new connector and use Data sources to create a datasource for the newly customised connector. Both of these features are available under Administration

  3. Process - use the custom connector to bring data into a process and use the query hook to filter the data. Use Kianda Designer to connect your data source to Kianda forms, for example a List control can connect to a datasource, see step 9 in List control.

What’s next Idea icon

To create a test service, follow the 3 steps above, or if you have already created a microservice go straight to steps to create a custom connector to learn how to create a custom connector.

1 - Microservice Creation

What is a Microservice

A microservice is a self contained independent service, usually hosted in the cloud, which can integrate with applications through REST APIs. Microservices allow developers to focus on developing a service without worrying about dependencies.

A microservice can be developed using almost any programming language.

Prerequisites

Before you get started, check that the following prerequisites are in place:

  • Ability/permissions to create resources with a cloud platform, for example, Google Cloud, AWS or Azure
  • Administration role or permissions within an Admin group within Kianda, see Users and Groups for more information
  • Visual Studio installed or other text editor for your chosen programming language

Microservice Requirements

Your microservice should implement the following three functions:

  1. Test
  2. Metadata
  3. Query

It is required that the microservice uses security best practices, to encrypt sensitive data using AES encryption and verify that each request and response is secure using a HMAC signature.

Microservice Development

The following steps for development will use Microsoft Azure cloud as an example, however, other cloud computing platforms with similar features can be used resulting in different naming conventions. The key steps are:

  1. Create a serverless Azure function app in Visual Studio by creating a new project, searching for Azure functions and using this as a template to get started. As well as the pre-defined libraries in the template, you must also add the following:

    using System.Security.Cryptography;
    using System.Text;
    using Newtonsoft.Json.Linq;
    using System.Collections.Generic;
    using System.Linq;
    
  2. Implement the functions for Test, Metadata and Query. Click on each of the links to read more. The steps highlighted in bold are unique to the specific function, while the other steps are repeated in all three functions.

  3. Debug locally and test using a tool such as Postman or download Kianda Cloud Connect.

  4. Deploy to Azure.

Test

The Test function ensures that the user is authorized to use the datasource. If they are not authorized then this function can be used to authorize the user and retrieve an access token for any further requests.

The steps involved in creating the test function are as follows:

  1. Deserialize the data in the custom connector request - see schema for details and Test sample code.

  2. Decrypt the encrypted settings property bag using the shared secret key and the Decrypt function, see Encryption/Decryption sample code for an example.

  3. Authorize the user and retrieve bearer token for subsequent requests and save the token to the settings property bag.

  4. Add the oauth token to the settings property bag and encrypt using your secret key.

  5. Create custom connector response and include success message to indicate test succeeded/failed.

  6. Sign the response using the EncryptdatawithHMACSHA256 method and include in the custom connector response, see Encryption/Decryption sample code for an example.

To help you get started, see sample code for the Test function by going to Test sample code.

Metadata

The Metadata function provides the list of available endpoints in the microservice and is called when selecting the datasource in a Kianda process, for example use a particular datasource to populate a List field in a Kianda form.

Connector tree

The steps involved in creating the metadata function are as follows:

  1. Deserialize the data in the custom connector request - see schema for details.

  2. Decrypt the encrypted settings property bag using the shared secret key and the Decrypt function see Encryption/Decryption sample code for an example.

  3. Create a JSON tree structure similar to the one found at Tree schema.

  4. Create custom connector response and include success message to indicate success/failure.

  5. Create a signature string using the EncryptdatawithHMACSHA256 method and include in the custom connector response, see Encryption/Decryption sample code for an example.

To help you get started, see sample code for the Metadata function by going to Metadata sample code.

Query

The Query function is where the execution of the datasource methods occurs, the metadata function provides the list of available methods.

The steps involved in creating the query function are as follows:

  1. Deserialize the data in the custom connector request - see schema for details.

  2. Decrypt the encrypted settings property bag using the shared secret key and the Decrypt function - see Encryption/Decryption sample code for an example.

  3. Get the query object from the request - see schema for details, and call the function related to the query in the datasource .

  4. Create custom connector response and include success message to indicate query succeeded/failed.

  5. Create the query result object - see schema for details, and include in the custom connector response.

  6. Sign the response using the EncryptdatawithHMACSHA256 method and include in the custom connector response, see Encryption/Decryption sample code for an example.

To help you get started, see sample code for the Query function by going to Query sample code.

Security

AES256

Advanced Encryption Standard (AES) is the technique used to encrypt sensitive data. This is a symmetric encryption technique, which means the same key is used to encrypt and decrypt the data, it is also a 2-way encryption algorithm.

AES Symmetric Encryption

HMAC

Hash based Message Authentication Code (HMAC) is used to ensure query responses are coming from the correct end point. HMAC is also an encryption algorithm but unlike AES it’s one-way encryption, meaning it isn’t decrypted on the other side. It is used for verification, for example if we encrypt a secret key using HMAC with a request id and then do the same in the response, we can verify that we have got the response back from the correct endpoint and know that it has not been tampered with because both hashes will match. The hash used is SHA256.

With AES we are encrypting and decrypting data and this method is fast and secure for large data, while with HMAC we use it to simultaneously verify both the data integrity and authenticity of a message.

What is HMAC

Schemas required for Custom Connectors

Custom Connector Request

	public class CustomConnectorRequest
        {
            public string subscriptionId { get; set; }
            public string userId { get; set; }
            public string requestId { get; set; }
            public string action { get; set; }
            public string encryptedSettingsPropertyBag { get; set; }
        	public byte[] iv { get; set; }
            public Query query { get; set; }
        }
Request Query Object
   public class Query {
        public string id { get; set; }
        public string action { get; set; }
        public JObject info { get; set; }
        public string orderBy { get; set; }
        public bool orderAscending { get; set; }
        public string paging { get; set; }
        public int rowLimit { get; set; }
        public List<string> fields { get; set; }
        public Dictionary<string, object> mappings { get; set; }
        public string filter { get; set; }
        public string filterBy { get; set; }
        public string filterMode { get; set; }
        public string signature { get; set; }

    }

Custom Connector Response

public class CustomConnectorResponse
    {
        public string requestId { get; set; }
        public string signature { get; set; }
        public string encryptedSettingsPropertyBag { get; set; }
    	public byte[] iv { get; set; }
        public QueryResult queryResult { get; set; }
    }
Response QueryResult
public class QueryResult
        {
            public bool success { get; set; }
            public string message { get; set; }
            public JObject data { set; get; }
            public List<JObject> items { get; set; }
            public string resultCount { get; set; }
            public string signature { get; set; }
        }

Retrieving URLs for Custom Connector settings

When you have created the Test, Metadata and Query functions and your microservice is running, you should receive an output similar to the following shown using Azure functions:

Azure function URLs

These URLs will be used in the Connector Settings tab when creating the Custom Connector.

What’s next Idea icon

Once your Microservice is deployed, you are ready to start creating a custom connector go to steps to create a custom connector.

2 - Sample schemas

Introduction

This page features sample schemas for parameters used in the Query Code tab when creating a customised data connector.

Each section below features exemplar schemas and parameters to help you when creating code for your hooks in the Query Code tab.

The Metadata hook in the Query Code tab typically looks like the following, using two parameters: tree and datasource.

metaData(tree, datasource) {
    return tree;
  }

Sample schemas for tree and datasource are available by clicking on the links.

Tree schema

{
    "text": "Countries And Cities",
    "name": "countriesAndCities",
    "icon": "fa fa-database",
    "selectable": false,
    "nodes": [
        {
            "text": "Countries",
            "icon": "",
            "desc": "",
            "nodes": [
                {
                    "text": "Country Name",
                    "name": "countryName",
                    "type": "string",
                    "desc": "",
                    
                },
                {
                    "text": "ISO Country Code",
                    "name": "ISOCode",
                    "type": "string",
                    "desc": ""
                }
            ]
        },
        {
            "text": "Cities",
            "name": "cities",
            "icon": "",
            "desc": "",
            "nodes": [
                {
                    "text": "City Name",
                    "name": "cityName",
                    "type": "string",
                    "desc": ""
                },
                {
                    "text": "ISO Country Code",
                    "name": "ISOCode",
                    "type": "string",
                    "desc": ""
                }
            ]
        }
    ]
}

To return to Microservice development, click on the Microservice link.

Datasource schema

Note: The same structure below can be used used in both Query hooks and Query success hooks, although data will vary slightly.

{
  "id": "2fe2d2c7-4feb-4c92-ac4c-fed4623d2d6e",
  "title": "Demo Connector",
  "type": "client",
  "typeIcon": "http://localhost:4171/public-file/322f14a6-63a0-4c68-b53e-4ca041c0e9ae/Geo-Connector-Icon.png",
  "typeTitle": "Demo Connector",
  "candelete": false,
  "readOnly": false,
  "status": "ready",
  "useConnector": false,
  "connectorId": "",
  "clientConnectorId": "19275478-68a9-43be-b8fb-54dc310cc0d6",
  "settings": {},
  "modified": "2022-11-11T15:12:44.173Z",
  "enableB2B": true,
  "enableFiltering": false,
  "b2bMappings": [],
  "modifiedBy": "5650d471-8c41-49b6-8f72-b77dddf3b956",
  "admins": [],
  "allowedUsers": [],
  "exclusionUsers": []
}

The Query hook in the Query Code tab typically looks like the following, using four parameters: datasource, query, rule and process.

 query(datasource, query, rule, process) {
     return query;
  }

Sample schemas each are available by clicking on the relevant links: datasource, query, rule, and process are available by clicking on the links.

Query schema

{
   "action": "select",
   "info": {
      "text": "Cities",
      "name": "Cities",
      "type": "item"
   },
   "rowLimit": 100,
   "conditions": [
      {
         "group": "and",
         "conditions": [
            {
               "id": "b3fe6a0b-c455-4560-abde-b5b7291d1ca8",
               "operator": "eq",
               "arg1": {
                  "name": "countryName",
                  "text": "Country Name",
                  "title": "Country Name",
                  "icon": "",
                  "type": "string",
                  "desc": ""
               },
               "arg2": {
                  "text": "Name",
                  "name": "name",
                  "type": "field",
                  "id": "07b1e8ab-9260-478c-a3e2-487aa0ee56f8",
                  "fieldType": "fields/field-textbox",
                  "date1": null,
                  "date2": null,
                  "value": "London"
               },
               "group": "and"
            }
         ],
         "id": "b6597e4d-60f1-44da-863a-e15287f72f1a"
      }
   ],
   "orderAscending": false
}

Rule schema

Note: This is similar to the rule used in the Query success hook.

The following sample rule schema is for a find-items rule which maps the response from the connector query to a table, notice the inputmapping and output mapping.

{
  "title": "Find items 3",
  "action": "rules/rule-finditems",
  "originalId": "5e6d1abc-1c5f-471c-b1f2-4bc038d7aefd",
  "enabled": true,
  "schedule": "default",
  "settings": {
    "inputMapping": [
      {
        "lefterror": "has-error",
        "righterror": "has-error"
      }
    ],
    "outputMapping": [
      {
        "left": {
          "text": "Country Name",
          "name": "country-name",
          "id": "6070ad52-7e4e-4a96-aa6b-320266644d93",
          "type": "field",
          "fieldType": "fields/field-textbox",
          "selectable": true,
          "icon": "fa fa-text-height"
        },
        "right": {
          "text": "Country Name",
          "name": "countryName",
          "type": "datafield"
        },
        "lefterror": "",
        "righterror": ""
      },
      {
        "left": {
          "text": "ISO Country Code",
          "name": "ISOCode",
          "id": "c2246c9a-af60-4fd0-9ac6-3195c456d3ff",
          "type": "field",
          "fieldType": "fields/field-textbox",
          "selectable": true,
          "icon": "fa fa-text-height"
        },
        "right": {
          "text": "ISO Country Code",
          "name": "ISOCode",
          "type": "datafield"
        },
        "lefterror": "",
        "righterror": ""
      }
    ],
    "errorMapping": [],
    "rowLimit": 100,
    "list": {
      "text": "Countries",
      "name": "Countries",
      "type": "item"
    },
    "mapToTable": "yes",
    "offlineCache": "no",
    "overrideTable": "yes",
    "serverPaging": "no",
    "table": {
      "text": "Table 1",
      "name": "form1-f4",
      "id": "7746f138-a913-4438-ba65-4905d8453d57",
      "type": "field",
      "fieldType": "fields/field-table",
      "selectable": true,
      "icon": "fa fa-table",
      "nodes": [],
      "visible": true,
      "rows": [
        {
          "text": "row1",
          "name": "form1-f4-row1",
          "id": "219e1d44-a997-4f76-b815-f409fdcb4774",
          "type": "field",
          "fieldType": "fields/table-row",
          "selectable": false,
          "nodes": [
            {
              "text": "Country Name",
              "name": "country-name",
              "id": "6070ad52-7e4e-4a96-aa6b-320266644d93",
              "type": "field",
              "fieldType": "fields/field-textbox",
              "selectable": true,
              "icon": "fa fa-text-height"
            },
            {
              "text": "ISO Country Code",
              "name": "ISOCode",
              "id": "c2246c9a-af60-4fd0-9ac6-3195c456d3ff",
              "type": "field",
              "fieldType": "fields/field-textbox",
              "selectable": true,
              "icon": "fa fa-text-height"
            }
          ],
          "visible": true,
          "state": {
            "expanded": true
          }
        }
      ]
    },
    "lastExecuted": "2022-12-14T17:29:42.425Z"
  },
  "elseSettings": {},
  "hasElse": false
}

Process schema

Note: This is similar to the process used in the Query success hook.

{
  "processVersion": "1.1",
  "processName": "connector-example",
  "isCreated": false,
  "isOfflineCreated": false,
  "isOfflineUpdated": false,
  "uniqueID": "45758d2b-f713-44c9-81dc-0e4cb7618902",
  "title": "connector example",
  "type": "Process",
  "name": "connector-example",
  "desc": "",
  "version": "1.0",
  "modified": "2022-11-14T09:33:46.254Z",
  "created": "2022-11-11T15:13:28.108Z",
  "status": "form 1",
  "securityMode": null,
  "enableSecurity": false,
  "deleted": false,
  "rejected": false,
  "completed": false,
  "settings": {
    "keepRuleExecutionOrder": "yes",
    "buttonDisplayFlag": true,
    "mobileNav": "yes",
    "comments": "",
    "offline-tag": "45758d2b-f713-44c9-81dc-0e4cb7618902"
  },
  "partnerId": null,
  "instanceIDFormat": null,
  "customInstanceIDFormat": null,
  "isPartner": false,
  "isDraft": true,
  "publish": false,
  "allowNew": false,
  "visMode": null,
  "group": null,
  "fieldsUpdated": null,
  "modifiedBy": null,
  "createdBy": null,
  "currentForm": "5af0c34e-28ea-4404-bba3-a8cf2df8eaff",
  "partner": null
}

The Query success hook in the Query Code tab typically looks like the following, using four parameters: datasource, result, rule and process.

 querySuccess(datasource,result,rule,process) {
    // this.get("dataservice").mapSuccess(result,rule,process); //uncomment to use default mapping behaviour
   return result;
  }

Sample schemas each are available by clicking on the relevant links: datasource, result, rule, and process are available by clicking on the links.

Result schema

{
    "success": true,
    "items": [
        {
            "countryName": "England",
            "ISOCode": 123
        },
        {
            "countryName": "Ireland",
            "ISOCode": 124
        }
    ]
}

What’s next Idea icon

To discover how to use your customised data connector widget in Kianda process design, go to Kianda application designer.

To create other widgets go to Custom field widget, Custom dashboard widget and Custom rule widget pages to find out more.

3 - Steps to create a custom connector

Introduction

This section will detail how to create a new Custom Connector which is a customised data connector for your organisation.

Note: You must be have an Administrator or Developer role to create a custom connector.

Before you begin

Please note before you begin that there are three key steps that need to be implemented in order to create and use a customised connector.

  1. Microservice - create a microservice that will implement metadata, query and test functions, click on the Microservice link to get further details.

  2. Use Kianda features to create and test your customised connector - use Developer to register a new connector and use Data sources to create a connector datasource for the newly customised connector.

  3. Process - use the custom connector to bring data into a process and use the query hook to filter the data.

Register a new connector

To create a new customised connector, follow the steps below:

  1. From the left-hand side menu, go to Administration > Developer then within the Connectors panel click on New Connector.

New Connector

  1. In the Create Connector dialog box, add a Title and browse for an Icon URL for your connector. The Connector Unique ID is automatically generated by the system.

    Create connector dialog box

  2. Make sure to copy the secret key to a safe location where you will find it as it will be needed later. You can do this by a) clicking on Download TXT which will result in the Secret Key being downloaded as a CSV file or b) clicking on Copy Key to Clipboard, then click on OK when done.

  3. Ensure that this secret key is added to your Microservice code, see Encryption and Decryption sample code, where you need to replace the string in the GetSecretKey function as shown below:

     private static string GetSecretKey()
            {
                return "{secret key generated from Kianda}"; //REPLACE ME
            }
    
  4. After populating the initial dialog box, the next screen will show you the title of the connector and contains four tabs: Connector Settings, Settings UI, Settings Code and Query Code.

    Tabs to create a customised connector

    The details of these tabs are found in the following sections.

Connector Settings tab

This is where the Connector Title and Icon can be changed. These details will be seen in the Data connectors function, once the customised connector is created.

Connector Settings tab

The URLs for metadata, test and query can be edited here too. The Metadata, Test and Query URL’s can be populated when the microservice is created, see Create a Microservice link for more details.

For example when your Microservice is running you should receive an output similar to the following shown using Azure functions and use these URLs in the Connector Settings tab.

Azure function URLs

To save details click on Update, then click on Close to return to the Developer resources page.

Settings UI tab

The Settings UI sets the user interface for a customised data connector within the Data sources function, found within Administration in the left-hand pane. By default there is sample code in this tab. This code will be rendered when the datasource for this connector is activated.

<div class="form-group">
   <label class="control-label">Client ID</label>
   {{input type="text" value=datasource.settings.clientID class="form-control"}}
</div>
<div class="form-group">
   <label class="control-label">Readwrite </label>
   {{radio-button default=true name='readwrite' label='No' value='no' group=datasource.settings.readwrite}}
   {{radio-button name='readwrite' label='Yes' value='yes' group=datasource.settings.readwrite}}
</div>
<div class="form-group">
   <button type="button" class="btn btn-primary" {{action 'authorize'}}>
   {{#if datasource.settings.authorized}}
   Re-Authorize
   {{else}}
   Authorize
   {{/if}}
   </button>
</div>

This code creates the content on the screen seen below, available in the Data sources function on the Datasource details page. Within the class form-group listed in the code above there is an Environment setting with labels ‘Demo’ and ‘Live’ which can be seen in the screen below. Also note an Authorize button added in the code above which has an action ‘authorize’ which is handled by the code in the Settings Code tab.

Datasources details created from Settings UI tab

Other details on the screen above, like the name of the page Datasource details, Use Kianda Cloud Connect and buttons Test connection, Save, Security and Close are automatically part of the UI for customised data connectors. However, custom handlebars can be added for the settings of the connector datasource using code in the Settings UI tab, namely fields like Datasource Name and Client Key as shown in the image above. This principle works the same as Widget UI, which is seen when creating custom rule widgets or field widgets for example.

Settings Code

In a similar manner to creating custom rule widgets or field widgets where there are Widget code tabs, the connector widget has a tab called Settings Code which is used to create the JavaScript code for the settings UI.

By default there is code in this tab, for example the Authorize button shown in the Datasource details page above has an ‘authorize’ function which is the first segment of code below.

{
   actions: {
      authorize() {
         let clientId = this.get("datasource.settings.clientID");
         let redirectUri = 'https://app.kianda.com/index.html';
         let scope = 'openid User.Read.All Directory.Read.All ';
         var readWrite = this.get("datasource.settings.readwrite");
         if (readWrite === 'yes') {
            scope += 'Directory.ReadWrite.All Group.ReadWrite.All User.ReadWrite.All Directory.AccessAsUser.All';
         }
         this.set("scope", scope);
         let guid = uuidv4();
         let consentUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' +
            'client_id=' + clientId +
            '&response_type=code' +
            '&redirect_uri=' + redirectUri +
            '&response_mode=query' +
            '&scope=' + scope +
            '&state=' + guid +
            '&prompt=login';
         var authWindow = window.open(consentUrl, '_blank', 'height=750,width=700,scrollbars=yes,location=yes');

         try {
            authWindow.focus();
         } catch (e) {
            $.notify('Error during authentication, please try again', {
               status: 'warning'
            });
         }
         var that = this;
         var handleCallback = function (evt) {
            if (that.isDestroyed !== true && evt.origin.indexOf('kianda.com') > -1 && evt.data) {

               let accessCode = evt.data;

               let scope = that.get("datasource.settings.scope");
               let openid = that.get("datasource.settings.openid");
               that.set('datasource.settings.accessCode', accessCode);
               that.set('datasource.settings.accessToken', null);
               that.set('datasource.settings.refreshToken', null);
               that.set('datasource.settings.authorized', false);
               that.set("datasource.settings.scope", scope);
               that.datasource.save().then(function () {
                  that.sendAction('test', that.datasource);
               });
            }


            window.removeEventListener('message', handleCallback);
         };
         window.addEventListener('message', handleCallback, false);
      }
   }


}

Other aspects to note in this sample code are the datasource settings, for example datasource.settings.accessToken and datasource.settings.environment denoting that you can add whatever settings you want to the UI within the Datasources details page using this type of code. These settings could be then used in a Microservice to create an action based on a chosen setting.

Query Code

When using a data source, all data sources will have built-in metadata, query and querySuccess functions to allow content searches, including metadata content or information about the content, such as title, author and creation date. The Query code tab contains the code needed for these three functions. Default code is provided to help you get started, see below.

{
  metaData(tree, datasource) {
    return tree;
  },
  query(datasource, query, rule, process) {
    return query;
  },
  querySuccess(datasource,rule,result,process) {
     //this.get("dataservice").mapSuccess(result,rule,process); //uncomment to use default mapping behaviour
    return result;
  }
}

The default code contains three hooks that allow you to tap in to a module to trigger certain behaviours or actions. These hooks are:

Click on each of the links above to see further details.

In Ember, bubbling will search for each of the actions in the code above, and if not found, then a default action is used, but if found the default is overwritten. This principle is useful when something is created multiple times which is the case when creating custom data connectors.

Metadata hook

The Metadata hook is where the return of the tree or metadata call can be altered for a chosen datasource used in Kianda form design.

The treeparameter as part of this hook is seen in the sample code snippet below.

  metaData(tree, datasource) {
    return tree;
  }

An example of the tree structure is shown below.

The tree follows the structure that there is a root node, in this example Countries and Cities is the root. The tree can be customised by adding an icon and making nodes selectable.

{
  "text": "Countries And Cities",
  "name": "countriesAndCities",
  "icon": "fa fa-database",
  "selectable": false,
  "nodes": [
    {
      "text": "Countries",
      "icon": "",
      "desc": "",
      "nodes": [
        {
          "text": "Country Name",
          "title": "Country Name",
          "name": "countryName",
          "type": "string",
          "desc": ""
        },
        {
          "text": "ISO Country Code",
          "title": "ISO Country Code",
          "name": "ISOCode",
          "type": "string",
          "desc": ""
        }
      ],
      "fields": [
        {
          "text": "Country Name",
          "title": "Country Name",
          "name": "countryName",
          "type": "string",
          "desc": ""
        },
        {
          "text": "ISO Country Code",
          "title": "ISO Country Code",
          "name": "ISOCode",
          "type": "string",
          "desc": ""
        }
      ]

    },
    {
      "text": "Cities",
      "name": "cities",
      "icon": "",
      "desc": "",
      "nodes": [
        {
          "text": "City Name",
          "title": "ISO Country Code",
          "name": "cityName",
          "type": "string",
          "desc": ""
        },
        {
          "text": "ISO Country Code",
          "title": "ISO Country Code",
          "name": "ISOCode",
          "type": "string",
          "desc": ""
        }
      ],
      "fields": [
        {
          "text": "City Name",
          "title": "ISO Country Code",
          "name": "cityName",
          "type": "string",
          "desc": ""
        },
        {
          "text": "ISO Country Code",
          "title": "ISO Country Code",
          "name": "ISOCode",
          "type": "string",
          "desc": ""
        }
      ]
    }
  ]
}

This structure will result in the output as shown in the image below,

Connector tree

Query Hook

The Query hook allows a query to the datasource to be customised. Parameters are passed into this function to allow customisation to happen. These parameters are: datasource, query, rule and process; sample schemas are available for each at the Sample schema link.

In the sample code below, the filter is obtained by drilling into the query conditions object. This is just one example of how the query can be customized before being processed, which is an advantage of using custom connectors. Take a look at the query schema for more information

 query(datasource, query, rule, process) {
    if(query.conditions)
    {
      query.filter = query.conditions[0].conditions[0].arg2.value;
    }
    return query;
  }

This ties in with the query success hook which handles the result which is returned from the datasource, which can also be customized.

QuerySuccess hook

The idea of the query success hook is to be able to customize the response of a datasource query for example drill into a complicated json response based on a condition.

querySuccess(datasource,result,rule,process) {
    // this.get("dataservice").mapSuccess(result,rule,process); //uncomment to use default mapping behaviour
   return result;
  }

Once the connector is created, the connector is available from the Data sources function under Administration, see Creating a datasource for details.

4 - Sample code for a microservice

Sample code

The following sections provide some sample code to help you get started in creating Test, Metadata and Query functions for your Microservice so that you can test and deploy a Custom Connector in Kianda, allowing you to connect forms and use data from any data source.

Encryption and Decryption sample code

        public static string HashWithHMACSHA256(string key, string value)
        {

            using (var hash = new HMACSHA256(Convert.FromBase64String(key)))
            {
                var hashedByte = hash.ComputeHash(Encoding.UTF8.GetBytes(value));
                var hashed = Convert.ToBase64String(hashedByte);
                return hashed;
            }
        }

        public static string AESEncrypt(string key, string plainData, out byte[] iv)
        {
            var _key = Convert.FromBase64String(key);
            using (Aes aes = Aes.Create())
            {
                aes.GenerateIV(); 
                aes.Mode = CipherMode.CBC;
                iv = aes.IV;
                aes.KeySize = 256;
                aes.Key = _key;
                using (var encryptor = aes.CreateEncryptor())
                {
                    using (MemoryStream ms = new MemoryStream())
                    {
                        using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                        {

                            using (StreamWriter sw = new StreamWriter(cs))
                                sw.Write(plainData);
                        }
                        return Convert.ToBase64String(ms.ToArray());
                    }
                }

            }
        }


        public static string AESDecrypt(string key, string encryptedData, byte[] iv)
        {
            var buffer = Convert.FromBase64String(encryptedData);
            var _key = Convert.FromBase64String(key);
            using (Aes aes = Aes.Create())
            {
                aes.KeySize = 256;
                aes.Mode = CipherMode.CBC;
                aes.Key = _key;
                aes.IV = iv;
                using (var decryptor = aes.CreateDecryptor())
                {
                    using (MemoryStream ms = new MemoryStream(buffer))
                    using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader sw = new StreamReader(cs))
                            return sw.ReadToEnd();
                    }
                }
            }

        }

        public static string GenerateAESKey()
        {
            using (Aes aes = Aes.Create())
            {
                aes.KeySize = 256;
                aes.GenerateKey();
                return Convert.ToBase64String(aes.Key);
            }
        }

        private static string GetSecretKey()
        {
            return "{secret key generated from Kianda}"; //REPLACE ME
        }

To return to Microservice development, click on the Microservice link.

Test sample code

        [FunctionName("connectorTest")]
        public static async Task<CustomConnectorResponse> connectorTest(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)][FromForm] HttpRequest req,
            ILogger log)
        {
            string SECRET_KEY = GetSecretKey(); 
            string signature = string.Empty;
            CustomConnectorResponse response = new CustomConnectorResponse();
            response.queryResult = new QueryResult();
            try
            {
                string testBody = await new StreamReader(req.Body).ReadToEndAsync();
                CustomConnectorRequest data = JsonConvert.DeserializeObject<CustomConnectorRequest>(testBody);
                var settings = JsonConvert.DeserializeObject<JObject>(AESDecrypt(SECRET_KEY, data.encryptedSettingsPropertyBag, data.iv));
                var accessCode = settings["accessCode"];
                var clientID = settings["client_key"];

                JObject tokenRequestObj = new JObject
                {
                    ["grant_type"] = "authorization_code",
                    ["client_id"] = clientID,
                    ["redirect_uri"] = "https://app.kianda.com/index.html",
                    ["code"] = accessCode
                };
                signature = HashWithHMACSHA256(SECRET_KEY, data.requestId);
                byte[] iv;
                var encryptedSettings = JsonConvert.SerializeObject(settings);
                var settingsobj = AESEncrypt(SECRET_KEY, encryptedSettings, out iv); //encrypt the settings
                response.encryptedSettingsPropertyBag = settingsobj;

                response.iv = iv;
                response.signature = signature;
                response.queryResult.success = true;
                response.queryResult.message = "Test completed successfully";
            }
            catch (Exception ex)
            {
                log.LogError(ex.Message);
                CustomConnectorResponse result1 = new CustomConnectorResponse();
                return result1;

            }

            return response;
        }

To return to Microservice development, click on the Microservice link.

Metadata sample code

         [FunctionName("connectorMetadata")]
        public static async Task<CustomConnectorResponse> connectorTree(//metadata
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            string SECRET_KEY = GetSecretKey();
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            CustomConnectorRequest data = JsonConvert.DeserializeObject<CustomConnectorRequest>(requestBody);
            // Do Request for token and update settings object 
            var settings = JsonConvert.DeserializeObject<JObject>(AESDecrypt(SECRET_KEY, data.encryptedSettingsPropertyBag, data.iv));

            JObject tokenRequestObj = new JObject
            {
                ["grant_type"] = "authorization_code",
                //["client_id"] = clientID,
                ["redirect_uri"] = "https://app.kianda.com/index.html"
                //["code"] = accessCode
            };
            //settings = GetToken(tokenRequestObj, settings);

            //create the Queryresult 
            CustomConnectorResponse result = new CustomConnectorResponse();
            QueryResult resultQuery = new QueryResult();
            result.queryResult = resultQuery;


            //create the return Tree 
            string signature = string.Empty;
            List<JObject> CityFields = new List<JObject> { };
            CityFields.Add(new JObject { ["name"] = "cityName", ["title"] = "City Name", ["text"] = "City Name", ["icon"] = "", ["type"] = "string", ["desc"] = "", });
            CityFields.Add(new JObject { ["name"] = "ISOCode", ["title"] = "ISO Country Code", ["text"] = "ISO Country Code", ["icon"] = "", ["type"] = "string", ["desc"] = "" });
            List<JObject> CountryFields = new List<JObject> { };
            CountryFields.Add(new JObject { ["name"] = "countryName", ["text"] = "Country Name", ["title"] = "Country Name", ["icon"] = "", ["type"] = "string", ["desc"] = "", });
            CountryFields.Add(new JObject { ["name"] = "ISOCode", ["text"] = "ISO Country Code", ["title"] = "ISO Country Code", ["icon"] = "", ["type"] = "string", ["desc"] = "" });

            List<JObject> nodes = new List<JObject> { };
            nodes.Add(new JObject { ["name"] = "Countries", ["text"] = "Countries", ["icon"] = "fa fa-globe", ["type"] = "STRUCTURE", ["nodes"] = new JArray { CountryFields }, ["fields"] = new JArray { CountryFields }, ["selectable"] = true });
            nodes.Add(new JObject { ["name"] = "Cities", ["text"] = "Cities", ["icon"] = "fa fa-globe", ["type"] = "STRUCTURE", ["nodes"] = new JArray { CityFields }, ["fields"] = new JArray { CityFields }, ["selectable"] = true });

            //create the tree root to be returned
            List<JObject> root = new List<JObject> { };
            root.Add(new JObject
            {
                ["text"] = "Countries and Cities",
                ["name"] = "CountriesAndCities",
                ["icon"] = "fa fa-database",
                ["selectable"] = false,
                ["nodes"] = new JArray { nodes }

            });

            try
            {
                result.queryResult.items = root;
                result.signature = HashWithHMACSHA256(SECRET_KEY, data.requestId);
                result.queryResult.success = true;
                result.queryResult.message = "Metadata Action Completed";
                byte[] iv;
                var encryptedSettings = JsonConvert.SerializeObject(settings);
                var settingsobj = AESEncrypt(SECRET_KEY, encryptedSettings, out iv); //encrypt the settings
                result.iv = iv;
                result.encryptedSettingsPropertyBag = settingsobj;
            }
            catch (Exception ex)
            {
                CustomConnectorResponse resultExc = new CustomConnectorResponse();
                QueryResult resultQueryExc = new QueryResult();
                result.queryResult = resultQuery;
                resultExc.queryResult.success = false;
                resultExc.queryResult.message = "Exception occured; " + ex.Message;
                resultExc.signature = signature;
                return resultExc;
            }

            return result;
        }

To return to Microservice development, click on the Microservice link.

Query sample code

    [FunctionName("connectorQuery")]
    public static async Task<CustomConnectorResponse> connectorQuery(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        string SECRET_KEY = GetSecretKey();
        CustomConnectorResponse result = new CustomConnectorResponse();
        QueryResult resultQuery = new QueryResult();
        result.queryResult = resultQuery;
        string signature = string.Empty;
        var myresponsestring = string.Empty;
        try
        {
            string testBody = await new StreamReader(req.Body).ReadToEndAsync();
            CustomConnectorRequest data = JsonConvert.DeserializeObject<CustomConnectorRequest>(testBody);
            Query query = data.query;
            var settings = JsonConvert.DeserializeObject<JObject>(AESDecrypt(SECRET_KEY, data.encryptedSettingsPropertyBag, data.iv));

            //demo object list creation

            result.queryResult.items = new List<JObject>();

            if (query.info.Value<string>("text") == "Countries")
            {
                List<JObject> CountriesList = new List<JObject>();
                var country1 = new JObject { ["countryName"] = "England", ["ISOCode"] = 123 };
                var country2 = new JObject { ["countryName"] = "Ireland", ["ISOCode"] = 124 };
                CountriesList.Add(country1);
                CountriesList.Add(country2);

                result.queryResult.items = CountriesList;
            }
            else
            {
                List<JObject> CitysList = new List<JObject>(); //create a list of jobjects 
                var city1 = new JObject { ["countryName"] = "England", ["cityName"] = "London" };
                var city2 = new JObject { ["countryName"] = "England", ["cityName"] = "Liverpool" };
                var city3 = new JObject { ["countryName"] = "Ireland", ["cityName"] = "Dublin" };
                var city4 = new JObject { ["countryName"] = "Ireland", ["cityName"] = "Cork" };
                CitysList.Add(city1);
                CitysList.Add(city2);
                CitysList.Add(city3);
                CitysList.Add(city4);

                //check for conditions then return accordingly
                if (query != null && !string.IsNullOrEmpty(query.filter))
                {
                    //filtering the city list depending on the filter 
                    var j = CitysList.Where(x => x.GetValue("countryName").Value<string>() == query.filter).ToList();
                    result.queryResult.items = j;
                }
                else
                {
                    // if no filter return the full list of citys
                    result.queryResult.items = CitysList;
                }
            }
            byte[] iv;
            var settingsobj = AESEncrypt(SECRET_KEY, JsonConvert.SerializeObject(settings), out iv); //encrypt the settings
            result.iv = iv;
            result.encryptedSettingsPropertyBag = settingsobj;
            result.signature = HashWithHMACSHA256(SECRET_KEY, data.requestId);
            result.queryResult.success = true;

        }
        catch (Exception ex)
        {
            CustomConnectorResponse resultExc = new CustomConnectorResponse();
            QueryResult resultQueryExc = new QueryResult();
            resultExc.queryResult.success = false;
            resultExc.queryResult.message = "Exception occured; " + ex.Message;
            resultExc.signature = signature;
            return resultExc;
        }

        return result;
    }

To return to Microservice development, click on the Microservice link.

What’s next Idea icon

To learn how to create a custom connector view the steps to create a custom connector.