Helper functions in the Betty Blocks CLI

A guide on how to locally test your action steps in the Betty Blocks low-code environment using helpers

Helper functions allow testing inside your coding environment. Since they are executed in an isolated environment (using Isolated-vm), they cannot directly access common Node.js functionalities such as HTTP or filesystem operations. To overcome this limitation, the Betty Blocks CLI includes a set of pre-defined helper functions.

This article explains each available helper function in the Betty Blocks CLI, its purpose, and how to use it in your projects.

Pre-requisites

To create and test your action steps in your low-code environment, you need to know/ have the following:

  • Javascript (ES6)
  • Isolated-VM installed
  • Betty Blocks CLI (find out how to install it here)
  • Have an initialized custom action step environment
  • Prior knowledge of helpers and test files

What are helper functions?

Helper functions are small, reusable functions that perform specific, often repetitive tasks inside an application. In this situation, they are inside the Betty Blocks CLI. They are designed to test your code locally.

How to test your code?

To test your action steps, you need to first, run the following command in your command line tool:

$ bb functions test

This will run all the .test.js files in the /test folder created in the directory you get after initiating the custom actions steps environment. If you don't have it, please visit our article on how to make your custom action step here.

List of helper functions available:

These are all external helper functions inside your Local Betty Blocks CLI environment, you configure them inside the .test.js files that are run using the $ bb functions test command:

1. Connecting to the data API

The gql helper lets you interact with the data API by sending GraphQL queries and mutations. This is useful for retrieving or updating data in your application, it makes use of CRUD

Read:

const query = `

  query {

    onePost(where: $where) {

      id

      title

    }

  }

`;

const {data, errors} = await gql(query, { where: { id: { eq: id } } });

 

Create:

const mutation = `

  mutation {

    createPost(input: $input) {

      id

    }

  }

`;

const {data, errors} = await gql(mutation, { input: { title: "New Post" } });

 

Update

const mutation = `

  mutation {

    updatePost(id: $id, input: $input) {

      id

    }

  }

`;

const {data, errors} = await gql(mutation, { id: id, input: { title: "Updated Post" } });

 

Delete

const mutation = `

  mutation {

    deletePost(id: $id) {

      id

    }

  }

`;

const {data, errors} = await gql(mutation, { id: id });


2. Generating DOCX files

With the generateDocx helper, you can generate docx files. This helper uses the docxtemplater package.


The generateDocx helper expects the following arguments: a URL for the template file, an object containing the data you want to use, and the docx options used by the docxtemplater. It returns a promise that once resolved, results in an ArrayBuffer. You can use the storeFile helper to store this ArrayBuffer as a file in the asset store.

Example:

interface Options {

  delimiters?: { start: string; end: string };

  paragraphLoop?: boolean;

  parser?(tag: string): Parser;

  errorLogging?: boolean | string;

  linebreaks?: boolean;

  nullGetter?(part: Part): any;

}


const generateDocx = async (

  templateUrl: string,

  data: Record<string, unknown>,

  docxOptions: Options

): Promise<ArrayBuffer>


See the documentation of docxtemplater for further information about the options.


Example function:


const generateDocument = async ({

  templateUrl: { url: templateUrl },

  model: { name: modelName },

  property: [{ name: propertyName }],

  fileName,

}) => {

  const buffer = await generateDocx(

    templateUrl,

    {

      users: [

        {

          firstName: 'John',

          lastName: 'Doe',

          email: null,

        },

        {

          firstName: 'Jane',

          lastName: 'Doe',

          email: 'jane.doe@test.test',

        },

      ],

      inline: "<i><b>hello world</b></i>",

      html: "<h1>Hello World!</h1>",

      image: "https://docxtemplater.com/xt-pro-white.png",

      secondImage: "https://docxtemplater.com/xt-pro-white.png",

    },

    {

      linebreaks: true,

      paragraphLoop: true,

    }

  );


  const reference = await storeFile(modelName, propertyName, {

    contentType:

      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',

    extension: 'docx',

    fileName,

    fileBuffer: buffer,

  });


  return {

    reference: reference,

  };

};


export default generateDocument;


3. Creating HTTP(S) requests


As functions run from a sandboxed environment, it's impossible to use libraries or functions that try to connect to other services over HTTP(S). Instead, we supply a wrapper function, fetch, around node-fetch which lets you call those services over HTTP(S). For documentation on how to use this function, it's best to look at the documentation of node-fetch.

Example:

const response = await fetch('https://example.com');

const text = await response.text();

In this example, we are fetching from an example URL and saving the response in a new constant called text.


Form data:

The data we received can be saved in an array using Form data. In a new constant, we can add a method: GET/POST, headers, and then the formData with an array giving a key and value.  An optional type and fileName can also be added.


const response = await fetch('https://example.com/file.pdf');


const blob = await response.blob();


const buffer = blob.buffer;


const request = {

  method: 'POST',

  headers: { 'X-Auth-Token': 'testKey' },

  formData: [

    { key: 'text', value: 'Example text' },

    { key: 'secondFile', type: 'buffer', value: buffer, fileName: 'file.pdf' },

  ],

};


const response = await fetch('https://example.com', request);


4. Logging

console.log

The console.log helper lets you print messages or debug information in the application logs. For more clarity, you can use different “console.” types:

Note: the console.debug might not show by default. For this to show, look for the log levels settings and turn on the verbose level.

The first logs look the same but can be used for filtering. In the log-level settings, different logs can be turned on or off, which can be useful when using a lot of logs.

5. Running actions


We provided the helper function runAction so you can run an action (in this scenario often referred to as a "sub-action") in the foreground.

The resulting value of that sub-action will be used later in your "main action.


Using the runAction helper

The runAction helper requires an object with the following keys:

  • id - the UUID of the (sub) action to be executed
  • input - an object containing input variable values for the (sub) action

As the helper function is an async function, you will have to await the function call. The return value of the function call is the result of the (sub) action.


For both the action ID and input variables we have provided the function option type Action and InputVariableMap respectively.


We have created a runAction Block Store function of which you can find the implementation here:

runAction

The runAction helper allows you to execute other actions (sub-actions) inside your application.

Example:

const subAction = async ({ action: id, input }) => ({

  result: await runAction({ id, input }),

});


export default subAction;


This is the corresponding function.json (its blueprint):

{

  "description": "Execute an action",

  "label": "Sub Action",

  "category": "Misc",

  "icon": {

    "color": "Orange",

    "name": "LightingIcon"

  },

  "options": [

    {

      "meta": {

        "type": "Action",

        "validations": { "required": true }

      },

      "name": "action",

      "label": "Action",

      "info": "The (sub) action that needs to be executed"

    },

    {

      "meta": {

        "type": "InputVariableMap",

        "action": "action"

      },

      "configuration": {

        "dependsOn": [

          {

            "option": "action",

            "action": "CLEAR"

          }

        ]

      },

      "name": "input",

      "label": "Input Variables",

      "info": "The input variables passed to the (sub) action"

    },

    {

      "meta": {

        "type": "Output",

        "output": {

          "type": "Inherit",

          "action": "action"

        }

      },

      "name": "result",

      "label": "As",

      "info": "The resulting value of the (sub) action"

    }

  ],

  "yields": "NONE"

}


6. Sending emails

To connect to SMTP servers and send e-mails the runtime has the SMTP helper. This helper connects to an SMTP server of your choice and sends an e-mail.

The SMTP helper expects the following arguments: 

  • an object containing the SMTP configuration 
  • an object containing the details for the e-mail you want to send. 

It returns a promise that once resolved, results in an object containing information about the sent e-mail.



Example:

interface Credentials {

  host: string;

  port: number;

  username: string;

  password: string;

  secure?: boolean;

  ignoreTLS?: boolean;

  requireTLS: boolean;

}

interface Options {

  sender: {

    from: string;

    replyTo?: string;

  };

  recipient: {

    to: string | string[];

    cc?: string | string[];

    bcc?: string | string[];

  };

  subject: string;

  body: string;

  attachments?: {

    filename: string;

    path: string;

  }[];

}

const smtp = async (credentials: Credentials, mailDetails: Options): Promise<SMTPPool.SentMessageInfo | Error>


The helper function uses Nodemailer, see SMTP transport: Nodemailer for documentation on the SMTP credentials.

Function example:

const sendMail = async ({

  host,

  port,

  username,

  password,

  from,

  replyTo,

  to,

  cc,

  bcc,

  subject,

  body,

}) => {

  const result = await smtp(

    {

      host,

      port,

      username,

      password,

      requireTLS: true

    },

    {

      sender: {

        from,

        replyTo,

      },

      recipient: {

        to,

        cc,

        bcc,

      },

      subject,

      body,

      attachments: [

        {

          filename: 'The name and extension for the file',

          path: 'Url of the file',

        },

      ]

    }

  );

  return { result };

};

export default sendMail;

7. Executing SQL queries

The SQL helper lets you execute raw SQL queries against databases.

Warning! Please, be aware that using this helper function requires extra CAUTION in terms of the security and integrity of your database. Take the necessary safety measures to avoid threats like SQL injection or unwanted table or record deletions. Use at your own risk.

Example:


interface DBCredentials {

  host: string;

  port: number;

  user: string;

  password: string;

  database: string;

  options?: {

    encrypt?: boolean;

  }

}


interface Options {

  client?: string; // default mysql

  raw: string;

  timeout?: number; // default 20s

}


const result = await sql(credentials: DBCredentials, options: Options);

This helper makes use of KnexJS. We accept the following database libraries:

Pg, Mysql, Mysql2, Oracledb, Tedious.

Example function:

const queryMsSQL = async () => {

  const result = await sql(

    {

      host: '127.0.0.1',

      port: 3306,

      user: 'your_database_user',

      password: 'your_database_password',

      database: 'myapp_test',

    },

    {

      raw: 'SELECT * FROM empDepartment;',

      client: 'mssql',

    }

  );

  return {
    Result,
  };
};

export default queryMsSQL;

For more information check the following link: Installation | Knex.js.


8. Storing files

To store files in the asset store and use the file in a file or image property, the runtime has the storeFile helper.

 

Example:

const storeFile = async (

  modelName: string,

  propertyName: string,

  file: string | {

    contentType: string;

    extension: string;

    fileBuffer: ArrayBuffer;

    fileName: string;

  }

  options?: {

    headers: { [key: string]: string };

  }

): Promise<string>

Storing files from a URL

To store files from a URL this helper downloads the file, generates a file reference, uploads the file to the asset store, and finally returns the file reference. You can assign the returned file reference to a file or image property in a create or update.


The storeFile helper expects three string arguments: the model name, the property name you want to upload a file for (this should be a file or image property), and the URL of the file you want to store. It returns a promise which, once resolved, results in the file reference as a string. You can enter a fourth optional argument to pass headers which is an array of key-value pairs (Where key is the name of the header and the value is the actual value itself.)


Example:

const uploadFile = async ({

  model: { name: modelName },

  property: [{ name: propertyName }],

  url,

}) => {

  const headers = [{ key: 'X-Auth-Token', value: 'token' }];

  const reference = await storeFile(modelName, propertyName, url, { headers });

  return {

    reference,

  };

}

 

From ArrayBuffer

Besides storing files using a URL this helper can also be used to store files using an ArrayBuffer. The only difference is the last argument, instead of a string this is now an object with the following structure:


{

  contentType: string;

  extension: string;

  fileBuffer: ArrayBuffer;

  fileName: string;

}



Example:


const file = await fetch(url);

const fileRef = await storeFile(

  "Import",

  "file",

  {

    contentType: "text/csv",

    extension: "csv",

    fileBuffer: file.blob().buffer,

    fileName: "Employee",

  },

  { headers: {} }

);



9. Parsing data

The parseData helper makes it easier to parse different datatypes in an Action. The function takes in an object with keys; data and format, and returns a Promise with a list of objects. The data keys can be one of these: a string with the data or a URL pointing to the data.

Example:

type JSONValue = string | number | boolean | null | JSONObject | JSONArray;

interface JSONObject {

  [x: string]: JSONValue;

}

type JSONArray = Array<JSONValue>;

type Format = 'CSV' | 'JSON';

interface ParseDataArguments {

  data: string;

  format?: Format;

}

async function parseData({

  data,

  format,

}: ParseDataArguments): Promise<JSONValue[]>;

 

Example from JSON file:

const data = `[

  {"data": 1, "description": ""},

  {"data": 2, "description": ""}

]`;

await parseData({

  data,

  format: 'JSON',

});


Example from CSV file:

const data = `order_id;customer;paid;created_at

14987576452870392979;John Doe;true;2213-05-22T16:08:55.067342584Z

638002794201564283;Jane Doe;true;2149-05-03T23:22:31.389391892Z`;

await parseData({

  data,

  format: 'CSV',

});


Example from API:

const url = 'https://httpbin.org/json';

await parseData({

  url,

  format: 'JSON',

});



Best practices

Error handling: Always wrap your helper function calls in try...catch blocks to handle potential errors.


For example:

try {

  const data = await gql(query);

  console.log(data);

}  

catch (error) {

  console.error("Error fetching data:", error);

}

 

Security warning! Avoid exposing sensitive information (e.g., tokens, credentials) in logs or unencrypted communications.

By following these steps, you’ll be able to test the custom action steps of your Betty Blocks application. For more information on helpers please visit our CLI Wiki.

Do you want to discuss the creation of your action step with other developers? Check out our community!