You are reading the documentation for an outdated Corteza release. 2024.9 is the latest stable Corteza release.

Extension Development

The goal of this section is to provide a detailed overview over the automation script development process, while omitting low-level details about the system itself.

The section also provides some tips and tricks for faster, better and more secure development process.

This section is stand alone, meaning it’s independent of the previous section [ext-facility].

Naming Conventions

We define the following set of naming conventions for a more consistent experience across different extensions:

@wip

File names

File names should be written in pascal case, ending with a .js extension (eg. SetLabel.js),

conv1

thing…​

Helper Classes

Helper classes consist of methods that greatly reduce the needed effort and the amount of required code to perform a specific operation, so it’s recommended to use them as much as possible. These classes are also context aware, meaning that the classes are able to determine some base parameters on their own, such as the current module, record and namespace.

Example of creating a new record with the help of Compose helper class (note how we don’t need to specify the namespace):

Compose.makeRecord({ Title: 'Lead title' }, 'Lead')
  .then(myLead => Compose.saveRecord(myLead))
  .then(mySavedLead => {
    mySavedLead.recordID // saved recordID
  })

All helper classes are available in the automation script’s execution context:

ctx.Compose

Provides functions to work with Corteza Low Code, such as accessing, creating and deleting records. See the definition for details,

ctx.ComposeUI

Provides functions to work with user interface for Corteza Low Code, such as opening record pages and showing notifications. See the definition for details,

ctx.Messaging

Provides functions to work with Corteza Messaging, such as creating channels and creating messages. See the definition for details,

ctx.System

Provides functions to work with Corteza core system, such as finding and creating users and managing role membership. See the definition for details.

Automation Script Anatomy

To better understand the process of implementing custom functionalities with the automation system, we define and describe the anatomy of automation scripts, and establish a few rules. An automation script is considered valid if:

  • a given .js file is structured under client-scripts or server-scripts directory,

  • a given .js file exports exactly one JS object with the export default keyword,

  • a given script defines at least one valid trigger or iterator definition,

  • a given script defines the security context if the script is a deferred or sink script,

  • a given JS object defines an exec (args, ctx) function.

Invalid automation scripts are still processed by the Corredor, but are excluded from further operations. Detected errors are visible in the Corredor logs. This allows for easier debugging.

Overview of script object properties and function

label

Automation script’s label, used to provide a user-friendly identification of the automation script,

description

A more verbose automation script description,

security

Defines automation script’s security context and role based filtering,

triggers

Defines automation script’s triggers as propery or method. Not compatible with iterator,

iterator

Defines automation script’s iterator as property or method Not compatible with triggers,

exec

Execution function that is invoked when the automation script is triggered.

Security

Script’s security property allows definition of user who’s credentials will be used when executing the script. It also allows access control by defining list of allowed and denied roles that can execute it.

security.runAs

Define security context for the automation script,

security.allow

Explicitly define what roles have access to the automation script,

security.deny

Explicitly define what roles do not have access to the automation script,

security.runAs parameter is only valid for server scripts.

Exec function

The execution function defines the actual code for the automation script, and conforms to the following interface:

interface ScriptFn {
  (args: exec.Args, ctx?: exec.Ctx): unknown;
}

Execution arguments

Execution arguments (first argument) contain the arguments that the automation script can work with, such as a newly created record, updated module, deleted page, …​ Arguments depend on the automation script’s event and resource (see [ext-resevt]).

Execution context

Execution context (second argument) contains contextual information about the execution, such as security context, helper class instances, API clients, loggers, …​

ctx.console

console object that can be used for logging. When running in the UA, this will be native window.console. When running in Corredor, this will be a Pino instance,

ctx.log

Shortcut for ctx.console.log,

ctx.$authUser

User object corresponding to the security context,

ctx.SystemAPI

Full blown API for Corteza System interaction,

ctx.ComposeAPI

Full blown API for Corteza Compose interaction,

ctx.MessagingAPI

Full blown API for Corteza Messaging interaction,

ctx.System

Helper class instance for the Corteza System,

ctx.Compose

Helper class instance for the Corteza Compose,

ctx.ComposeUI

Helper class instance for the Corteza Compose user interface,

ctx.Messaging

Helper class instance for the Corteza Messaging,

ctx.frontendBaseURL

Base URL used by front-end web applications. This is useful when generating URL’s inside server scripts.

Automation script execution result

When the script’s execution is complete, it should provide one of the following results:

Unknown Error

An error aborts the script’s execution chain (the current script and all following scripts).

Aborted Error

An error with the value of 'Aborted' stops the execution of the current automation script. Any further scripts down the chain are executed as usual.

false

A return value of false stops the execution of the current automation script. Any further scripts down the chain are executed as usual.

unknown

Any other return value specifies that the execution was successful and the following script (if any) can execute.

If the resource defines a before event variant, the return value will be used as the updated version of that resource. For example: if the automation script is defined with the resource type of compose:record and returns the updated version of the args.$record value, the existing version is replaced by the updated version.

Server script vs. client script

There is a small deviation when it comes to client and server script executions. One of the differences is that, when it comes to client scripts, the arguments are provided as a reference, meaning that any changes to the resource are reflected to the original object. Because of that the resource does not need to be returned in order for the changes to take effect.

For example, running the following code in the client script’s exec function will reflect the values without the need of returning the updated record.

{
  ...
  async exec ({ $record }) {
    $record.values.Field1 = 'value1'
    $record.values.Field2 = 10
  },
}

If your script should be able to revert the changes, in case of an error, you should use an intermediate object for the new values. For example:

{
  ...
  async exec ({ $record }) {
    const v = { ...$record.values }
    v.Field1 = 'value1'
    v.Field2 = 10

    try {
      await apiOperation($record)
    } catch (e) {
      throw new Error(e)
    }

    $record.setValues(v)
  },
}

Automation Trigger Anatomy

To better understand the power of automation triggers and their definition, we define and describe the anatomy of automation triggers.

Automation triggers are constructed with the following methods:

t.on

Defines the event type, such as 'manual', 'request' (see [ext-resevt]).

t.for

Defines the resource type, such as 'compose:record' (see [ext-resevt]),

t.where

Defines the constraints that must match in order for the automation script to be executed (eg. t.where('module', 'Lead')). Refer to Constraints for details,

t.before

Defines that the automation script is executed before a specific operation occurs,

t.after

Defines that the automation script is executed after a specific operation occurs,

t.at

Defines that the automation script is executed at a specific time,

t.every

Defines the interval in which the automation script is executed,

t.uiProp

Defines the visual representation of the manual automation trigger, such as its color and label.

Trigger and iterator are not compatible; you can only use one or the other within the same automation script.

Triggers will be evaluated in an isolated context outside of the actual automation script file. This prevents use of any constants or other symbols defiled outside of the trigger.

For example, the following trigger will cause:

const modName = 'Contact'
export default {
  triggers ({ on }) {
    return on('manual')
      .for(modName)
  },
  exec (args, ctx) {...}
}

When defining the automation script, you can use object destructuring to remove a few bits of code.

triggers (t) {
    return [ t.on(...)... ]
}

// can be replaced by

triggers ({ on }) { // any of the above mentioned methods
    return [ on(...)... ]
}

If the automation script contains just a single trigger, you can omit the array and just return the one.

triggers ({ on }) {
    return [ on(...)... ]
}

// can be replaced by

triggers ({ on }) {
    return on(...)...
}

Constraints

A constraint consists of 3 components:

  1. resource attribute name (see [ext-constraintResources]),

  2. operation,

  3. value.

Constraint operations

  • Equal: check for equality between the two values. Available operators:

    • eq

    • =

    • ==

    • ===

Equality check is default operator. When using only 2 parameters for constraint, "equal" operator is used

  • Not equal: negated check for equality between the two values. Available operators:

    • not eq

    • ne

    • !=

    • !==

  • Like: check for string equality, with support for wildcards. Available operators:

    • like

  • Not like: negated check for string equality, with support for wildcards. Available operators:

    • not like

Available wildcards:

  • one or more characters: %, *,

  • one character: _, ?.

  • Match: check for string equality, defined as a regular expression. Available operators:

    • ~

  • Not match: negated check for string equality, defined as a regular expression. Available operators:

    • !~

Automation Iterator Anatomy

To better understand the power of automation iterators and their definition, we define and describe their anatomy.

Automation iterator are constructed with the following methods:

i

Defines the resource, action and the filter used for resource fetching. Passed object must conform to the interface:

interface {
  resourceType: string; (1)
  eventType: 'onManual' | 'onInterval' | 'onTimestamp' = 'onManual'; (2)
  action: 'update' | 'delete' | 'clone' | '' = ''; (3)
  filter: IteratorFilter; (4)
}
1 define the resource type; see [ext-resevt] for details.
2 define the event type; see [ext-resevt] for details.
3 define what action the script performs; see Iterator actions for details,
4 define the filter to use; see Filter for details.
i.at

Defines that the automation script is executed at a specific time,

i.every

Defines the interval in which the automation script is executed,

Iterator actions

update

The result of the automation script will be used to update the original resource,

delete

The result of the automation script will be used to delete the original resource,

clone

The result of the automation script will be used to create a new resource with updated values; original resource is left unchanged,

Iterator script doesn’t perform any operation on the resource except if explicitly specified by the action property.

Iterator and trigger are not compatible; you can only use one or the other within the same automation script.

Filter

Iterator filter provides ability to write query (conditions), sorting, limit and offset rules It closely resembles Corteza’s filter object and conforms to the following interface:

interface {
  query: string; (1)
  sort: string; (2)
  limit: number | string; (3)
  offset: number | string; (4)
  [_: string]: number | string; (5)
}
1 SQL like query filter to use (WHERE <query>).
2 SQL like sort to use (ORDER BY <sort>).
3 Maximum number of resource.
4 Offset from first record.
5 additional non-standard resource specific parameters.

Client Scripts

Client scripts are automation scripts executed in the client’s browser (user agent; UA). Client scripts should be used when:

  • interaction with the user is required,

  • response latency should be minimal,

  • execution reliability is not important,

  • we are not working with sensitive information such as authentication tokens or api credentials,

  • automation script produces a light load on system resources.

Example use cases

  • prompt user to confirm form submission,

  • validate or modify form’s data before submission,

  • redirect the user to another page; either in Corteza Low Code or other,

  • open an external resource inside a popup window.

Trigger types

Client scripts support two types of triggers; explicit and implicit.

Explicit

Explicit triggers execute on a specific user invocation, such as a button press. These include:

Manual

Manual automation triggers are defined as buttons inside the user interface. They most commonly appear inside Low Code automation page blocks or inside Low Code record list toolbars. They are invoked and executed inside the web application.

Manual automation triggers don’t need to be defined as client scripts. When defined as a server script it will be invoked from the UA and executed from inside Corredor.

Implicit

Implicit triggers execute as a collateral to another system event such as form submission. These triggers include before/after events. For a full list of available events refer to [ext-resevt].

Server Scripts

Server scripts are processed, served and executed by Corredor server.

Server scripts should be used when:

  • interaction with the user is not required,

  • response latency is not as important,

  • execution consistency is important,

  • automation script produces a heavy load on system resources

Example use case

  • insert additional record fields based on an external data source,

  • send an email when a new user signs up,

  • run statistic operations once a month for reporting purposes.

Trigger types

Server scripts support a few different trigger types; explicit, implicit, deferred, iterators and sink.

Explicit

Explicit triggers execute on a specific user invocation, such as a button press. These include:

Manual

Manual automation triggers are defined as buttons inside the user interface. They most commonly appear inside Low Code automation page blocks or inside Low Code record list toolbars. They are invoked in UA and executed inside Corredor.

Implicit

Implicit triggers execute as a collateral to another system event such as record creation. These triggers include before/after events. For a full list of available events refer to [ext-resevt].

Deferred

Deferred automation triggers are executed at a specific point in time; once or multiple times, such as a reminder. These include:

Scheduled

scheduled triggers are executed at an exact time specified by a time stamp in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). This trigger is executed exactly once,

Interval

interval triggers are executed periodically in an interval defined by a cron expression (see robfig/cron package for details).

Deferred automation triggers are executed at most once every minute, so you should not define an interval or timestamp that uses higher precision (seconds or milliseconds).

Iterators

Iterators are executed similarly to deferred automation triggers (by a schedule or an interval). The main difference is, that the automation script is executed for each resource that matches the provided filter.

Sink

Sink automation triggers are executed on a specific http request. They can be used to implement new routes on the API, such as a web hook to some external service.

@todo signatures

Record Validation

Automation scripts provide the ability to validate records before they are created or updated. This helps us define a system that is more resilient to the dangers of malformed user input.

Record validation from a script can be performed by scripts that are executed on:

server scripts:
  • beforeCreate,

  • beforeUpdate.

client scripts:
  • beforeFormSubmit,

  • onFormSubmitError.

When we wish to warn the user about a value error, the flow differs from client and server scripts, but the result is the same — a set of validator.ValidatorError objects contained inside a validator.Validated object (refer to [coredev-compose-recordValidation] for details).

interface ValidatorError {
  kind: string;
  message: string;
  meta: { [key: string]: unknown };
}

interface Validated {
  set: ValidatorError[];
}

Server scripts

When we wish to provide value errors from server scripts, we simply throw an instance of validator.ValidatorError. For example:

import { validator } from '@cortezaproject/corteza-js'

export default {
  ...
  async exec ({ $record }) {
    if ($record.value.Field !== 'Super Specific Value') {
      throw new validator.ValidatorError({
        kind: 'invalidValue',
        message: 'You didn\'t inter the super specific value',
        meta: {
          field: 'Field',
          recordID: $record.recordID,
        },
      })
    }
  },
}

Client Scripts

When executing client scripts, two extra parameters are present:

ctx.validator

The parameter contains the compose.RecordValidator object, that can be used to validate the record this script is executing for.

ctx.errors

The parameter contains the validator.Validated object, that contains current errors, and provides a place to store new errors.

When we wish to provide value errors from inside client scripts, we either use the ctx.validator or construct a validator.ValidatorError object manually. The validator.ValidatorErrors should then be pushed into ctx.errors. For example:

import { validator } from '@cortezaproject/corteza-js'

export default {
  ...
  async exec ({ $record }, { errors, validator }) {
    const errs = new validator.Validated()
    if ($record.values.Field !== 'Super Specific Value') {
      errs.push(new validator.ValidatorError({
        kind: 'invalidValue',
        message: 'You didn\'t inter the super specific value',
        meta: {
          field: 'Field',
          recordID: $record.recordID,
        },
      }))
    }

    $record.values.FullName = `${$record.values.FirstName} ${$record.values.LastName}`
    errs.push(...validator.run($record).set)
    errors.push(...errs)
  },
}

When the validator is unable to find any errors, the returned validator.Validated contains an empty set. Since the value is always present, there is no need for if statements, such as

if (errs && !errs.valid()) {
  errors.push(...errs.set)
}