Tutorial

This tutorial will outline how to extract data from Subscribe-HR and then send it to a RESTful API end point.

Template Structure

The following array represents bare-bones configuration file that will be used to run our export. If you tried to run it now it would not pass validation because our main pipeline is empty. Following sections will detail creation of components in the file to create a fully working configuration.

var configuration = {
    RuntimeSettings: {
        Version: "1.0",
        Name: "SampleExport",
        LogPayload: true,
        EntryPipelineId: "Pipeline1"
    },
    Connections: {},
    Operations: {},
    Mappings: {},
    Functions: {},
    Pipelines: {
        Pipeline1: []
    }
};

Setting Up Connections

The first thing that we need to do is to add some connections. Operations cannot be performed without connections. One way to describe connections is they are client libraries designed to handle specific protocols. They establish line of communication to local or remote resources and allow operations to perform actions.

Because we are trying to export data from Subscribe-HR the first connection that needs to be created is Datum. Datum is the internal name of data layer that is used in the Subscribe-HR platform to handle all underlying data actions. Think of it as our proprietary ORM library.

Datum connection definition is simple because all information about it is already available through the system. It simply looks like this.

ShrConnection: {
    Type: "Datum",
    Name: "Datum Connection"
}

Next we need to create a connection to RESTful API. It requires couple more parameters than Datum connection. We provide base URL for all requests and authentication record Id so that our connection knows how to handle authentication.

ApiConnection: {
    Type: "Restful",
    Name: "RESTful API Connection",
    Url: "https://api.somesystem.com.au",
    Authentication: 1
}

Adding Operations

Having connections is great but not very useful without being able to perform operations. So what we will do next is to create some operations that we can use in execution pipelines.

Because we are trying to export some employee data, first operation will query it through Datum connection. Key parameters are Action and Query. We are executing SSQL query. Pagination parameter is also defined with max items set to 10 which means that if there are more than 10 records this operation will run in batches. It will not happen in this case because we are querying 2 employees only.

GetShrEmployees: {
    Name: "Get Shr Employees",
    Connection: "ShrConnection",
    Action: "Query",
    Query: "SELECT e FROM Employee e WHERE e.Id IN (71, 72)",
    ErrorHandlingStyle: "halt",
    Pagination: {
        MaxItemsPerPage: 10
    }
}

Now that first operation has been defined we can update our configuration template. It now looks like this. Also note that GetShrEmployees operation was added to Pipeline1 as first action.

var configuration = {
    RuntimeSettings: {
        Version: "1.0",
        Name: "SampleExport",
        LogPayload: true,
        EntryPipelineId: "Pipeline1"
    },
    Connections: {
        ShrConnection: {
            Type: "Datum",
            Name: "Datum Connection"
        },
        ApiConnection: {
            Type: "Restful",
            Name: "RESTful API Connection",
            Url: "https://api.somesystem.com.au",
            Authentication: 1
        }
    },
    Operations: {
        GetShrEmployees: {
            Name: "Get Shr Employees",
            Connection: "ShrConnection",
            Action: "Query",
            Query: "SELECT e FROM Employee e WHERE e.Id IN (71, 72)",
            ErrorHandlingStyle: "halt",
            Pagination: {
                MaxItemsPerPage: 10
            }
        }
    },
    Mappings: {},
    Pipelines: {
        Pipeline1: [
            {
                Type: "Operation",
                Id: "GetShrEmployees"
            }
        ]
    }
};

Now let’s run the above configuration to see what it does. To do that you will need to first create Process record in the system by going to Integration > Processes and clicking Create button. Enter process name and paste configuration into code editor. Press Save button. No errors should be generated at this stage as our configuration meets minimal requirements. At this point Run Process button will appear. Once the button is pressed you will see a loading icon. At this stage a message has been sent to the worker in the background to let it know that the process needs to run immediately. It may take few minutes for it to complete depending on the volume of data being processed. To see what’s going on with the process, go to Events tab. It will show all the actions that have been executed.

Note

While testing your configuration ensure that volume of data that is being sent or received is limit to few records only. It will make it easier to debug and save a lot of waiting time.

The following image shows entries in my Events tab after executing above configuration. Returned data can be seen in detailed view output field.

_images/sharpi_log.png

Adding Iterator

What are iterators? They help us to run through multiple records. Above example returns two employee records. If we were working with operation that supports importing multiple employees then at this stage we can just perform data transformation and call the operation. It however is not the case with a lot of APIs. From our experience the standard data flow is Get Data => Loop => Transform => Check If New / Existing => Create / Update. So let’s create an iterator for our two records.

{
    Type: "Iterator",
    Selector: "$.Data",
    OutputTag: "ShrEmployee"
}

Above example will iterate over $.Data[*]. You will also notice that the record gets tagged at this point. This is to ensure that if we need to access original data later on in the transformation process that it can easily be done without performing additional actions.

Adding Another Operation

OK so now we have two employee records that we loop over. As mentioned above at this point we probably want to check if this employee already exists in the destination system before trying to create it. There is a number of ways to do this. One, we can create a flag that tracks whether employee has already been exported or not. Two, we can try to always create it and just let it fail. If it fails we then trigger an update pipeline. Three, and this is the method I personally prefer as it is pretty fail safe, we check whether record already exists in destination system and then trigger appropriate pipeline. So let’s add an operation that checks if record exists or not.

LookupApiEmployee: {
    Name: "API Lookup Single Employee",
    Connection: "ApiConnection",
    Method: "GET",
    Path: "/api/v1/employees/:EmployeeId"
}

It can be seen in the definition above that this operation will require a URL parameter :EmployeeId to be passed in. This can be done using mappings or a function. I prefer mappings as relying on functions can make it harder to maintain configuration files.

Adding Mappings

Above operation requires EmployeeId parameter to be passed in for it to work correctly. We already know from RESTful Input that URL parameters can be passed using Parameters.Url attribute so we define some mappings to create this structure.

LookupApiEmployeeMappings: [
    {
        FromField: "$.Employee.EmployeeCode",
        ToField: "$.Parameters.Url.EmployeeId"
    }
]

Now let’s update our configuration file and add the latest changes.

var configuration = {
    RuntimeSettings: {
        Version: "1.0",
        Name: "SampleExport",
        LogPayload: true,
        EntryPipelineId: "Pipeline1"
    },
    Connections: {
        ShrConnection: {
            Type: "Datum",
            Name: "Datum Connection"
        },
        ApiConnection: {
            Type: "Restful",
            Name: "RESTful API Connection",
            Url: "https://api.somesystem.com.au",
            Authentication: 1
        }
    },
    Operations: {
        GetShrEmployees: {
            Name: "Get Shr Employees",
            Connection: "ShrConnection",
            Action: "Query",
            Query: "SELECT e FROM Employee e WHERE e.Id IN (71, 72)",
            ErrorHandlingStyle: "halt",
            Pagination: {
                MaxItemsPerPage: 10
            }
        },
        LookupApiEmployee: {
            Name: "API Lookup Single Employee",
            Connection: "ApiConnection",
            Method: "GET",
            Path: "/api/v1/employees/:EmployeeId"
        }
    },
    Mappings: {
        LookupApiEmployeeMappings: [
            {
                FromField: "$.Employee.EmployeeCode",
                ToField: "$.Parameters.Url.EmployeeId"
            }
        ]
    },
    Pipelines: {
        Pipeline1: [
            {
                Type: "Operation",
                Id: "GetShrEmployees"
            },
            {
                Type: "Iterator",
                Selector: "$.Data",
                OutputTag: "ShrEmployee"
            },
            {
                Type: "Map",
                Id: "LookupApiEmployeeMappings"
            },
            {
                Type: "Operation",
                Id: "LookupApiEmployee"
            }
        ]
    }
};

Quick summary of the changes in the pipeline:

  • Call GetShrEmployees operation which will return two employee records
  • Iterate over results
  • Map record to produce URL parameter
  • Call LookupApiEmployee to see if record already exists in the destination system

So far so good. Now how do we actually test result of the last operation. This is where logical functions can be very useful.

Adding Function

Because last operation is of type RESTful API the response will contain headers, status codes and response body. Refer to RESTful Output for more details. If the API end point is implemented correctly then we should receive status code 404 if record does not exist. Let’s define action of type function with inline function to test for it.

{
    Type: "Function",
    FunctionType: "Logical",
    Code: function(input) {
        if (input.StatusCode == "404") {
            return "Pipeline2";
        }
        return "Pipeline3";
    }
}

Deciphering the above. If response code is 404 then trigger Pipeline2 (creation of new record) otherwise go to Pipeline3 (update existing record).

Adding New Pipeline

Now we can add new pipeline to handle record creation. It only requires two actions, Map and Operation. Let’s create another operation and mappings to use in the new pipeline.

Note

Each subsequent action will inherit output of previous action. Tags can be used to work around this issue.

Adding create employee operation.

{
    CreateApiEmployee: {
        Name: "API Lookup Single Employee",
        Connection: "ApiConnection",
        Method: "POST",
        Path: "/api/v1/employees"
    }
}

Adding mappings.

MapShrEmployeeToApi: [
    {
        FromField: "$.Employee.Id",
        ToField: "$.Data[0].id"
    },
    {
        FromField: "$.Employee.Surname",
        ToField: "$.Data[0].surname"
    },
    {
        FromField: "$.Employee.FirstName",
        ToField: "$.Data[0].firstNames"
    },
    {
        FromField: "$.Employee.StartDate",
        ToField: "$.Data[0].startDate",
        DateFormatFrom: "Y-m-d",
        DateFormatTo: "d-M-Y"
    },
    {
        FromField: "$.Employee.Gender.Value",
        ToField: "$.Data[0].gender",
        Translations: {
            male: "Male",
            female: "Female"
        }
    }
]

Putting It All Together

The following configuration can now be used as a template for all integration processes. It should be expanded to add Pipeline3 which should looks very similar to Pipeline2 with small difference in mappings and operator call.

var configuration = {
    RuntimeSettings: {
        Version: "1.0",
        Name: "SampleExport",
        LogPayload: true,
        EntryPipelineId: "Pipeline1"
    },
    Connections: {
        ShrConnection: {
            Type: "Datum",
            Name: "Datum Connection"
        },
        ApiConnection: {
            Type: "Restful",
            Name: "RESTful API Connection",
            Url: "https://api.somesystem.com.au",
            Authentication: 1
        }
    },
    Operations: {
        GetShrEmployees: {
            Name: "Get Shr Employees",
            Connection: "ShrConnection",
            Action: "Query",
            Query: "SELECT e FROM Employee e WHERE e.Id IN (71, 72)",
            ErrorHandlingStyle: "halt",
            Pagination: {
                MaxItemsPerPage: 10
            }
        },
        LookupApiEmployee: {
            Name: "API Lookup Single Employee",
            Connection: "ApiConnection",
            Method: "GET",
            Path: "/api/v1/employees/:EmployeeId"
        },
        CreateApiEmployee: {
            Name: "API Lookup Single Employee",
            Connection: "ApiConnection",
            Method: "POST",
            Path: "/api/v1/employees"
        }
    },
    Mappings: {
        LookupApiEmployeeMappings: [
            {
                FromField: "$.Employee.EmployeeCode",
                ToField: "$.Parameters.Url.EmployeeId"
            }
        ],
        MapShrEmployeeToApi: [
            {
                FromField: "$.Employee.Id",
                ToField: "$.Data[0].id"
            },
            {
                FromField: "$.Employee.Surname",
                ToField: "$.Data[0].surname"
            },
            {
                FromField: "$.Employee.FirstName",
                ToField: "$.Data[0].firstNames"
            },
            {
                FromField: "$.Employee.StartDate",
                ToField: "$.Data[0].startDate",
                DateFormatFrom: "Y-m-d",
                DateFormatTo: "d-M-Y"
            },
            {
                FromField: "$.Employee.Gender.Value",
                ToField: "$.Data[0].gender",
                Translations: {
                    male: "Male",
                    female: "Female"
                }
            }
        ]
    },
    Pipelines: {
        Pipeline1: [
            {
                Type: "Operation",
                Id: "GetShrEmployees"
            },
            {
                Type: "Iterator",
                Selector: "$.Data",
                OutputTag: "ShrEmployee"
            },
            {
                Type: "Map",
                Id: "LookupApiEmployeeMappings"
            },
            {
                Type: "Operation",
                Id: "LookupApiEmployee"
            },
            {
                Type: "Function",
                FunctionType: "Logical",
                Code: function(input) {
                    if (input.StatusCode == "404") {
                        return "Pipeline2";
                    }
                    return "Pipeline3";
                }
            }
        ],
        Pipeline2: [
            {
                Type: "Map",
                InputTag: "ShrEmployee",
                Id: "MapShrEmployeeToApi"
            },
            {
                Type: "Operation",
                Id: "CreateApiEmployee"
            }
        ]
    }
};