Skip to main content

Creating a new Transformer

note

All transformers source code exists inside the packages repository.

The ITransformer Interface

This page elaborates on how to create a new type of transformer. A transformer "must" implement the ITransformer interface which contains a single method transformer(xmsg: XMessage): Promise<XMessage>.

Example Implementation

import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common/transformer.interface";

export class MyOwnTransformer implements ITransformer {

constructor(readonly config: Record<string, any>) { }

async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
};
}
// Perform any transform logic
const calculation = 2 + 2;
// Add the result to xmsg
xmsg.transformer.metaData!.calculationResult = calculation;
// Change the output state
xmsg.transformer.metaData!.state = 'outputState';
// Return the output xmessage
return xmsg;
}
}

Implementation

Implementing a transformer is a 4 step process:

  • Implementing a Transformer class.
  • Creating a config.json file for the Transformer.
  • Generating registry.json file.
  • Adding the Transformer class to the TransformerFactory.

Step 1: Implementing a Transformer Class

Transformers me belong to one of the 5 classes namely:

  • GenericTransformer
  • IfElseTransformer
  • SwitchCaseTransformer
  • StateRestoreTransformer
  • RetryTransformer

For creating a transformer create a new folder with the name of the transformer inside the relevant transformer class. Inside the folder create a new transformer file, for example, my_transformer.transformer.ts.

Creating a GenericTransformer class Transformer

A GenericTransformer can perform any generic function on an XMessage. A GenericTransformer works on 2 states, that is, the onSuccess and onError states. A transformer transform function must resolve successfully with a response XMessage or else reject the promise. If the transform function resolves, onSuccees target is called, else onError target is called.

Example Implementation

import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common/transformer.interface";

export class MyOwnGenericTransformer implements ITransformer {

constructor(readonly config: Record<string, any>) { }

async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
};
}
// Perform any transform logic
const calculation = 2 + 2;
// Add the result to xmsg
xmsg.transformer.metaData!.calculationResult = calculation;
// Return the output xmessage
return xmsg;
}
}

Creating an IfElseTransformer class Transformer

A IfElseTransformer outputs a binary if or else state.

Example Implementation

import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common";

export class MyOwnIfElseTransformer implements ITransformer {

constructor(readonly config: Record<string, any>) { }

async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
}
}
// Update the state in metaData
xmsg.transformer.metaData!.state = Math.random() > 0.5 ? 'if' : 'else';
return xmsg;
}
}

Creating a RetryTransformer class Transformer

A RetryTransformer outputs a retry or error state. It "must" update the metaData.retryCount property after retrying a transformer node.

Example Implementation

import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common";

export class MyOwnRetryTransformer implements ITransformer {

constructor(readonly config: Record<string, any>) { }

async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
}
}
// Check value of `retryCount`.
if (!xmsg.transformer.metaData!.retryCount || xmsg.transformer.metaData!.retryCount < (this.config.retries ? 0)) {
// Modify value of `retryCount`.
xmsg.transformer.metaData!.retryCount = (xmsg.transformer.metaData!.retryCount || 0) + 1;
// Return `retry` as the result state.
xmsg.transformer.metaData!.state = 'retry';
}
else {
// Delete `retryCount` and return `error` as result state.
delete xmsg.transformer.metaData!.retryCount;
xmsg.transformer.metaData!.state = 'error';
}
console.log(`SIMPLE_RETRY count: ${xmsg.transformer.metaData!.retryCount}`);
return xmsg;
}
}

Creating a SwitchCaseTransformer class Transformer

A SwitchCaseTransformer outputs an arbitary number of states.

Example Implementation

import { XMessage } from "@samagra-x/xmessage";
import { ITransformer } from "../common";

export class MyOwnSwitchCaseTransformer implements ITransformer {

constructor(readonly config: Record<string, any>) { }

async transform(xmsg: XMessage): Promise<XMessage> {
if (!xmsg.transformer) {
xmsg.transformer = {
metaData: {}
}
}
const myStates = ['these', 'are', 'my', 'states'];
// Update the state in metaData
xmsg.transformer.metaData!.state = myStates[Math.floor(Math.random() * 4)];
return xmsg;
}
}

Step 2: Creating a config file

A config file is a file that contains the meta data about a transformer. The data inside a config file is added to the global registry.json file. In the same folder parallel to the my_transformer.transformer.ts file create a config.json file. The spec for creating a config.json file is validated using zod and is as follows.

const transformerClassEnum = z.nativeEnum(TransformerClass);
const transformerTypeEnum = z.nativeEnum(TransformerType);

const transformerSpec = z.object({
name: z.string().min(1),
class: transformerClassEnum,
type: transformerTypeEnum,
description: z.string().min(1),
config: z.object({
required: z.record(z.string().min(1), z.string().min(1)),
optional: z.record(z.string().min(1), z.string().min(1)),
conditional: z.record(
z.string().min(1),
z.object({
type: z.string().min(1),
ifAbsent: z.string().min(1).array().optional(),
ifPresent: z.string().min(1).array().optional(),
}),
),
}),
version: z.string(),
});

Here is an example of CodeRunner Transformer config file.

{
"name": "Code Runner Transformer",
"class": "GenericTransformer",
"type": "CODE_RUNNER",
"description": "A code runner capable of running custom JS code.",
"config": {
"required": {
"code": "string"
},
"optional": { },
"conditional": { }
},
"version": "0.0.1"
}

Step 3: Running the registry generator

Inside the root folder of the transformers folder, there exists a generator script called registry_generator.ts which is responsible for extracting all config files and adding it to the registry.json file. The registry.json is an auto-generated file and must not be modified manually. You need to run this script to validate that your config file is correct and add it to the registry automatically.

To run the script simply use this command:

npx tsx registry_generator.ts

If the script executes without any error, then congratulations, your config file is correct.

note

Do not forget to add registry.json file to your git commit or the CI will fail as it compares the latest registry by running the generator script.

Step 4: Adding your transformer to the Factory

Inside the common/transformer.types.ts file, you need to declare your transformer as one of the transformer types. For this, add a new entry in TransformerType enum as well as to TransformerMapping object which maps your transformer type to a class.


Once this is done you need to open XStateFactory in Orchestrator and look for getTransformerObject() function. This function is responsible for creating an instance of your Transformer class. Go ahead and add an entry for your new TransformerType.


Once this is all wired up, you should be ready to use your own Transformer ๐ŸŽ‰.