Custom functions

Introduction to how to use custom functions in mobile extensions.

Overview

The mobile extension standard flow allows custom forms to fetch and save data using GraphQL. This works for most use cases. However, it does not support advanced requirements, such as fetching data from multiple data sources (external systems) using REST APIs or transforming data before or after fetching and saving.

To address these limitations, the mobile extension custom functions are built to provide a solution to support businesses’ needs. They allow users to do the following things:

  • Customize how their custom form fetches and saves data.
  • Use both GraphQL and REST APIs to fetch or save data.
  • Apply custom business logic using TypeScript.

Limitations

  • Data size: The response data must be smaller than 10MB. If it exceeds this limit, the custom function will throw an exception.
  • Execution time: The custom function must be executed within 30 seconds. If it takes longer, the custom function will throw an exception.

Exposed function

Mobile extension custom functions provide two types of outputs:

  • Frontend custom functions: Customize the UI of the custom form.
  • Backend custom functions: Customize the business logic of mobile extension backend functions, such as processing data before saving, modifying API responses, or integrating with external systems.

Mobile extension custom functions expose backend functions similar to those provided by the mobile extension service. They include the following functions:

  • Static fetch functions: Fetch shared data for a form. Since this data is scoped to the form, multiple jobs or resources can access the same data. It is typically used for retrieving common or reference data. Refer to Fetch static data.

  • Instance fetch functions: Fetch data for each instance, for example, a job or resource. Each job or resource has its own instance-specific fetch data per form.

  • Save functions: Save changes made in a custom form to the data source.

  • Online mode functions: Allow the custom form to search for data in real-time when online.

Create a custom function

To create a new custom function template, refer to the guide in the mex-custom-function-template repository. This repository is a Cookiecutter template that includes detailed steps to generate a basic mobile extension custom function project.

Make sure that Cookiecutter is installed before proceeding.

You can generate a new custom function automatically using the following command:

cookiecutter -s --no-input git@github.com:Skedulo/mex-custom-function-template.git project_name="$NAME" project_slug="$SLUG"

Once you’ve successfully created a custom function, it has the following structure:

├── node_modules
├── package.json
├── src
│   ├── __tests__
│   │   └── Sample.spec.ts
│   ├── buildscripts
│   │   ├── esbuild.backend.ts
│   │   └── esbuild.frontend.ts
│   ├── functions
│   │   ├── fetcher.ts
│   │   └── saver.t
│   ├── graphql
│   │   └── queries
│   │       └── queries.graphql
│   ├── index.backend.ts
│   ├── index.frontend.ts
│   ├── localDevIndex.ts
│   ├── params.ts
│   ├── tools
│   │   ├── config.json
│   │   ├── generate-graphql-ast.ts
│   │   ├── generate-graphql-types.ts
│   │   └── generate-info.ts
│   └── tracing.ts
└── tsconfig.json

Focus on the following files:

  • src/functions/*.ts: Implements the execution code for the custom function.

  • src/graphql/queries/queries.graphql: Defines GraphQL queries to fetch data.

  • src/params.ts: Configures the exposed functions of the custom function.

Input/output of a custom function

The input and output of a custom function may vary depending on the version of mex-service-libs it uses.

Backend custom function

Static fetch

  • Input:

    export type CustomFetchInput = {
        contextObject: string;
        contextObjectId: string;
        variables?: Variables;
    };
    export type CustomStaticFetchInput = CustomFetchInput;
    
  • Output:

    export type ObjectDataResult = {
        __typename: string;
        UID?: string;
        [key: string]: any;
    };
    
    export type CustomStaticFetchResult = {
        data: {
            [key: string]: ObjectDataResult | ObjectDataResult[];
        };
    };
    

Instance fetch

  • Input:

    export type CustomFetchInput = {
        contextObject: string;
        contextObjectId: string;
        variables?: Variables;
    };
    
  • Output:

    export type CustomStaticFetchResult = {
        data: {
            [key: string]: ObjectDataResult | ObjectDataResult[];
        };
    };
    

Save

  • Input:

    export type CustomSaveInput0_0_19 = {
        metadata: {
            [key: string]: any;
        };
        instanceData: {
            [key: string]: any;
        };
        newInstanceData: {
            [key: string]: any;
        };
        staticData?: {
            [key: string]: any;
        };
        objectMapping?: {
            [key: string]: string;
        };
        variables?: Variables;
    };
    
  • Output:

    export type CustomSaveResult = {
    objectMapping: { [key: string]: string }
    }
    

Online mode

  • Input:

    export type CustomSearchInput = {
    label: string
    variables?: Variables
    }
    
  • Output:

    export type ObjectDataResult = {
    __typename: string
    UID?: string
    [key: string]: any
    }
    
    export type CustomSearchResult = {
    data: ObjectDataResult[]
    }
    

Frontend custom function

A frontend custom function is different from a backend custom function. It is converted into pure JavaScript and executed on the mobile frontend.

A typical frontend custom function follows this format:

function FunctionName(customArg1, customArg2, ..., extras: Extras)
customArg1, customArg2 Arguments passed when building the UI Definition. There is no limit on the number of arguments.
extras The extras object is automatically provided by the engine when executing a custom function. It acts as a helper, offering built-in methods to handle data, UI interactions, and other utilities.

To export the method, add it to the end of the file:

let exportFns = {
  FunctionName: FunctionName
}

The result of the custom function depends on how you use it.

For example:

  • If you use the custom functions as an expression that requires a return of a boolean, then it returns a boolean.

  • If you use the custom function as a method that results in a certain behavior when users interact with a custom button, then no result needs to be returned.

  • If you use the custom functions in a localized key, then it returns a string.

Examples

The following example shows how to use the custom function in a localized key:

Setup for index.frontend.ts

function renderElapsedTime(pageData:any, {extHelpers, libraries}: Extras): string {
  let now = libraries.moment.utc(extHelpers.date.getNowDateTime())
  let lastCheckInDate = libraries.moment.utc(pageData.CheckInTime)

  let diff = libraries.moment.duration(now.diff(lastCheckInDate))

  let result = ""

  if (diff.days() > 0) {
    result += " " + extHelpers.data.translate('ElapsedTimeDays', diff.days())
  }

  if (diff.hours() > 0) {
    result += " " + extHelpers.data.translate('ElapsedTimeHours', diff.hours())
  }

  if (diff.minutes() > 0) {
    result += " " + extHelpers.data.translate('ElapsedTimeMinutes', diff.minutes())
  }

  if (result == "") {
    result = extHelpers.data.translate('CheckInJustMomentAgo')
  }

  return result
}

let exportFns = {
  renderElapsedTime: renderElapsedTime
}

How to use custom function inside en.json (localized key)

"ElapsedTimeValue": "${cf.renderElapsedTime(pageData)}"

For further information, refer to the following topic: Complex computation using custom functions.

Extras object

The Extras object provides various helper methods to boost development:

interface Extras {
  extHelpers: ExtHelper,
  libraries: Libraries
}

export interface ExtHelper {
    data: ExtHelperData
    date: ExtHelperDate
    ui: ExtHelperUI
}

interface ExtHelperData {
    changeData(fn: () => void):any /* Change data in data context */
    submit(options?: ExtHelperSubmitOptions): Promise<boolean> /* Submit/Save data from current page, and automatically close page */
    translate(key: string, args?: any[]): string /* translate key from localization keys */
}

type ExtHelperSubmitOptions = {
    stopWhenInvalid?: Boolean
}

interface ExtHelperDate {
    getNowDateTime(): string /* get current date time */
}

interface ExtHelperUI {
    alert(message: string): void /* make an alert UI */
}


interface Libraries {
  moment: any
}