Extensions
We define a set of automation scripts, resources and other assets (system and Low Code configurations) as an extension.
You can use our samples, and testing template to get setup faster. |
/package.json
/...
/server-scripts
/...
/client-scripts
/auth
/...
/admin
/...
/compose
/...
/messaging
/...
/shared
/...
/…
indicates that you are free to structure your files as you see fit.
/auth
, /admin
, /compose
, and /messaging
contain scripts specific to each web application.
/shared
contains code that client scripts can reuse.
We recommend grouping automation scripts based on their context.
For example, scripts working over leads could go into a |
When defining utilities (and other tool functions) inside the Most of the time, they will be invalid.
Use the |
Automation scripts
An automation is a piece of JavaScript code that implements some business logic and conforms to this interface:
export interface Script {
label?: string;
description?: string;
security?: {
runAs?: string;
deny: string[];
allow: string[];
}|string;
triggers: Trigger[];
exec?: ScriptFn;
}
One automation script per file. |
export default {
label: "label goes here",
description: "description goes here",
// Use the ones you need, delete the rest
triggers ({ before, after, on, at }) {
return before('event goes here')
.where('constraint goes here')
// Add/remove constraints here
},
// remove async if you aren't doing any async operations
// use object destructuring for args and ctx
async exec(args, ctx) {
// Code goes here
},
}
See server script samples, and client script samples for more. |
There are two main categories of automation scripts; server scripts and client scripts.
Server scripts |
These are executed in the Corteza Corredor server. Use server scripts when:
Example use cases:
|
Client scripts |
These are executed in the client’s browser (user agent, if you will). Use client scripts when:
Example use cases:
|
A rule of thumb — if we need to interact with the user (show notification, request confirmations), use client scripts. Else, use server scripts.
Treat client scripts as less secure (you can freely inspect their contents from the browser) and less reliable (a user can manually terminate their execution — closes the page). |
Script execution
The execution function (exec
) implements the business logic that the automation script should perform.
Any code that you want to execute should be directly in the execution function or referenced via importing.
interface ScriptFn {
(args: exec.Args, ctx?: exec.Ctx): unknown;
}
Execution arguments
The arguments (args
) differ based on the event that triggered the automation script.
Refer to resources and events for a complete list.
Arguments to a client-script are provided via references to the original objects, meaning that any change to the argument parameter will reflect to the original object.
Arguments to a server-script are provided as a copy of the original object, meaning that the changes will not be reflected on the original object.
Execution context
The context (ctx
) is static for all events.
ctx.console
-
console
provides a logger. Client scripts refer to thewindow.console
objects; server scripts refer to aPino
instance. ctx.log
-
Shortcut for
ctx.console.log
. ctx.$authUser
-
Auth user is a reference to the invoking user.
ctx.SystemAPI
-
API client for Corteza System interaction.
ctx.ComposeAPI
-
API client for Corteza Low Code interaction.
ctx.MessagingAPI
-
API client for Corteza Messaging interaction.
ctx.System
-
Corredor helper for the Corteza System.
ctx.Compose
-
Corredor helper for the Corteza Low Code.
ctx.ComposeUI
-
Corredor helper for the Corteza Low Code user interface.
ctx.Messaging
-
Corredor helper for the Corteza Messaging.
ctx.frontendBaseURL
-
Base URL used by front-end web applications. This is useful when generating URL’s that point to the Corteza applications.
Execution result
The execution result determines what should happen next.
- Unknown Error
-
Any unknown error (other than
Aborted
) terminates the current script execution and prevents the event from triggering any additional automation scripts. - Aborted Error
-
An error with the value of
'Aborted'
stops the execution of the current automation script. The event is able to trigger any additional automation scripts. false
-
Same as Aborted Error.
- unknown
-
Any other result indicates that the execution was successful and the next script can be triggered. What should happen next is relative to the automation script; see below sections for more details.
|
Implicit script execution result
The following only applies for before events. |
When a value is returned, that value is used instead of the original value.
export default {
// Use the ones you need, delete the rest
triggers ({ before }) {
return before('create', 'update')
.where('module','contact')
},
exec({ $record }, ctx) {
$record.values.FullName = `${$record.values.FirstName} ${$record.values.LastName}`
return $record
},
}
The above example calculates the FullName
property from the FirstName
and LastName
.
The return $record
instructs that the new version of the $record
should be used (the one with the FullName
calculated).
If we didn’t return anything (null
or undefined
for example) the change would be reverted.
The return value for after events is discarded (can not be updated). |
Sink script execution result
When a value is returned, that value is used as an HTTP response.
export default {
security: 'super-secret-user',
triggers ({ on }) {
return on('request')
.where('request.path', '/some/extension')
.where('request.method', 'GET')
.for('system:sink')
},
exec ({ $request, $response }, { Compose }) {
$response.status = 200
$response.header = { 'Content-Type': ['text/html; charset=UTF-8'] }
$response.body = `<h1>Hello World!</h1>`
return $response
},
}
The above example returns a simple static HTML document with a Hello World!
written over it.
You can use this to implement the OAuth protocol or a confirmation page.
This isn’t limited to simple HTML documents.
Just make sure that your responses are properly structured (content type, status, and body). |
export default {
security: 'super-secret-user',
triggers ({ on }) {
return on('request')
.where('request.path', '/model/roi')
.where('request.method', 'GET')
.for('system:sink')
},
async exec ({ $request, $response }, { Compose }) {
$response.status = 200
$response.header = { 'Content-Type': ['application/json'] }
let pl = {}
try {
pl.product = await fetchProduct($request.query.productCode[0])
pl.roi = await calculateRoi(pl.product)
} catch ({ message }) {
$response.status = 500
$response.body = JSON.stringify({
error: message,
})
return $response
}
$response.body = JSON.stringify(pl)
return $response
},
}
DevNote include other trigger types. |
Automation triggers
Automation triggers (trigger) let you define when the script should be executed (along with some extra bits).
See trigger samples.
The Corredor server evaluates the triggers in an isolated context, outside of any imports or variable definitions. The following example will not work:
|
Explicit |
These are explicitly triggered by pressing a button. Use explicit triggers when you wish to manually initiate something, such as an OAuth authentication flow, redirection to an external resource, or data export. |
||
Implicit |
These are implicitly triggered based on system events. Use implicit triggers when you wish to automatically do something when another thing occurs; such as sending an email when you register a new user; or adding a changelog entry when the content changes. Refer to resources and events for a complete list of events you can listen for. |
||
Deferred Server script only, requires explicit security context |
The system triggers these sometime in the future; either periodically (define with cron expressions), or at a timestamp (use ISO 8601; this one: Use deferred triggers when you want to repeat something or do something in the future; such as recurring payments or sending holiday newsletters to your subscribers.
|
||
Sink Server script only, requires explicit security context |
These are triggered by the system when it receives a request; either HTTP, or email. Use sink triggers when you want to respond to requests; such as webhooks for external services or custom API endpoints. For example capturing data from external forms, tracking external document changes, and capturing payments.
|
Defining the resource
The defined resource specifies the resource the automation script is executed for; for example a record, module, and user. Refer to resources and events for a complete list of available resources.
This is done by specifying a single .for(…)
call.
.for('ResourceGoesHere')
triggers ({ before }) {
return before('create', 'update')
// This will trigger for a compose record resource
.for('compose:record')
},
Defining constraints
Constraints let you define precisely when the automation script should execute.
This is done by chaining a series of .where(…)
calls.
.where(
resourceAttribute,(1)
comparator|value,(2)
[value],(3)
)
1 | The resource attribute that the constraint should check; resource constraints for a complete list. |
2 | When 2 arguments are provided, this is the value to check against; when 3 arguments are provided, this is the comparison operator. |
3 | When provided, this is the value to check against. |
triggers ({ before }) {
return before('create', 'update')
.for('compose:record')
// vv these two vv
.where('module', 'Lead')
.where('namespace', 'crm')
},
|
|||||
|
|||||
|
|||||
|
|||||
|
|||||
|
Automation trigger conventions
- Use object destructuring
-
Object destructuring helps you shorten the entire thing.
// Instead of using:
triggers (t) {
return t.after('create')
.for('compose:record')
.where('module', 'super_secret_module')
},
// you can do:
triggers ({ after }) {
return after('create')
.for('compose:record')
.where('module', 'super_secret_module')
},
// Neat, right?!
- Make trigger constraints as strict as possible
-
Having loose constraints can cause unwanted issues when there are multiple namespaces under the same instance. Two namespaces could easily define a module with the same handle, which would cause both of them to execute the given script.
// Instead of using:
triggers (t) {
return t.after('create')
.for('compose:record')
.where('module', 'super_secret_module')
},
// you can do:
triggers ({ after }) {
return after('create')
.for('compose:record')
.where('module', 'super_secret_module')
.where('namespace', 'super_secret_namespace')
},
Security Context
The security context lets you control script execution based on the invoking user and their roles.
Invoking user
The invoking user is someone who performed an action that triggered the script execution.
For example; you pressed a button, so you are the invoking user.
Security context lets you define the invoking user, to permit operations over resources that the actual user may not have access to. For example, you can permit regular users to access records via automation scripts, but not directly via the record list.
Deferred and sink scripts require you to specify the security context as the invoker is not known. |
// This security context forces the system to use some-user-identifier-here when executing the script
export default {
trigger (t) {...}
security: 'some-user-identifier-here',
exec (args, ctx) {...}
}
You can use the user’s handle, email or ID as the |
Forcing the invoking user is only available for server scripts. |
We suggest you create a new system user that is responsible for the script execution.
For example, our DocuSign extension requests a new |
Restricting script execution
Security context lets you prevent specific users from performing specific operations. For example, you can prevent regular users from signing documents or sending quotes.
-
allow
: specifies what roles are permitted to trigger the automation script. -
deny
: specifies what roles are not allowed to trigger the automation script.
If a user is not allowed to trigger an explicit script (a button), the button is shown as disabled. |
// This security context only permits the administrator and superuser to trigger the script.
// Other roles will not be able to trigger it.
export default {
trigger (t) {...}
security: {
allow: ['administrator', 'superuser'],
},
exec (args, ctx) {...}
}
// This security context permits all roles but the client and lead.
export default {
trigger (t) {...}
security: {
deny: ['client', 'lead'],
},
exec (args, ctx) {...}
}
API clients
API clients define an SDK to work with Corteza API.
They are provided in the execution context (here: exec(args, ctx)
— see Script execution).
All API operations are achievable via these clients. |
DevNote extract methods here? |
Corredor helpers
Corredor helpers implement the most common automation script operations, such as creating new records, registering users, and sending emails.
They are provided in the execution context (here: exec(args, ctx)
— see Script execution).
Corredor helpers are context-aware, meaning that they can automatically determine some arguments. For example; when creating a record, Corredor helpers will know what namespace, module, and record you are using.
Corredor helpers can be used outside of your automation scripts. If your application needs to interact with Corteza, you can use them. |
DevNote extract methods here? |
Modifying existing extensions
Create a modified copy
-
copy the extension source (clone the repository, or copy the files),
-
modify the source as you see fit,
-
deploy your version instead of the original version.
Overwrite the scripts
When the Corredor processes your automation scripts, they are assigned an auto-generated name, generated from the source file path.
# The CRM extension
/ server-scripts
/ crm
/ Lead
/ SetLabel.js
The SetLabel.js
script is assigned /server-scripts/crm/Lead/SetLabel.js:default
as the name.
To overwrite the SetLabel.js
script, you must define a script that will be assigned the same name (has the same path).
# The CRM extension
/ package.json
/ node_modules
/ server-scripts
/ crm
/ Lead
/ SetLabel.js # <- We're targeting this one
/ AnotherScript.js
# Your extension
/ package.json
/ node_modules
/ server-scripts
# To overwrite something in the CRM extension
/ crm
/ Lead
/ SetLabel.js # <- This version will replace the CRM version
# The rest of your code goes here
/ extension
/ Lead
/ SomeScript.js
For this to work, you must make sure that your extension is included after the extension you wish to modify. For example:
|
Using node modules
Corteza Corredor supports the use of external node modules, both on the server-scripts and client-scripts.
Corteza Corredor uses the Yarn package manager. |
-
manually insert it into the
package.json
file, -
run the
yarn add NAME_GOES_HERE
.
When you register and load the extension in the Corredor server, it will automatically resolve any changed dependency from the package.json
file.
We’re observing some anomalies when running Yarn inside a docker container. If you’re getting an error message similar to the one below, it means that Yarn was not able to install the dependencies. This error occurs when Yarn is unable to store its cache.
To fix this, you need to:
The dependencies should now be installed and available for use.
The above |
Node modules can then be used just like anywhere else.
{
"dependencies": {
"axios": "^0.18.0"
}
}
import axios from 'axios'
export default {
// ...
async exec() {
await axios.get(...)
// ...
}
}
Different extensions do not share their dependencies. If two extensions use the same dependency, they both need to define it. |
Testing
Extensions are essentially Node.js projects with some extra bits; meaning that the extensions can be tested in the same way.
You are free to use any testing framework and any testing methodology you wish.
DevNote provide some more examples for other frameworks? |
An example setup
/ .gitignore
/ .eslintrc.js
/ .mocharc.js
/ package.json
/ server-scripts
/ Sample.js
/ Sample.test.js
/ ...
/ client-scripts
/ ....
.vscode
node_modules
.nyc_output
coverage
yarn-error.log
module.exports = {
root: false,
env: {
node: true,
es6: true,
},
extends: [
'standard',
],
}
module.exports = {
require: [
'esm',
],
'full-trace': true,
bail: true,
recursive: true,
extension: ['.test.js'],
spec: [
'client-scripts/**/*.test.js',
'server-scripts/**/*.test.js',
],
'watch-files': [ 'src/**' ],
}
{
"scripts": {
"lint": "eslint {server-scripts,client-scripts}/**/* --ignore-pattern *.test.js",
"test:unit": "mocha",
"test:unit:cc": "nyc mocha"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"esm": "^3.2.25",
"mocha": "^7.0.1",
"nyc": "^14.1.1",
"sinon": "^8.1.1"
},
"nyc": {
"all": true,
"reporter": [
"lcov",
"text"
],
"include": [
"client-scripts/**/*.js",
"server-scripts/**/*.js"
],
"exclude": [
"**/*.test.js"
],
"check-coverage": true,
"per-file": true,
"branches": 0,
"lines": 0,
"functions": 0,
"statements": 0
}
}
export default {
/* istanbul ignore next */
trigger ({ before }) {
return before('create')
},
exec () {
return 'Hello World!'
}
}
Note this part:
|
import { expect } from 'chai'
import Sample from './Sample'
describe(__filename, () => {
describe('Sample exec result', () => {
it('should return a string', () => {
expect(Sample.exec()).to.eq("HelloWorld")
})
})
})
package.json
defines three scripts:-
lint
: lint the code using the default ES6 standard (can be configured; see here), -
test:unit
: unit test the code with your.test.js
files (can be configured in the.mocharc.js
file), -
test:unit:cc
: unit test the code and return a code coverage report.
The code coverage report gets generated into the |
Inspect the I usually use the http-server package to help with this, but a simple "Open in <browser name here>" will do.
|
DevNote: provide some complex examples using Sinon and promises. |
Deploying extensions
Setting up
To use the extension, it must be available to the Corredor server, either locally or on the server.
If you are running the Corredor without Docker (from source code), you can skip any Docker related steps. |
-
your current working directory is where your extension is,
-
your server deploy directory is
/opt/deploy/test-project
, -
your file structure looks like this (where your Corteza is running):
data/
docker-compose.yml
.env
-
Create a new directory for the extension; we’ll name it
corredor
, but the name doesn’t matter. -
Somehow transport the extension source files into the newly created directory (see below sections for tips).
-
Add a new volume to the
docker-compose.yaml
file that will contain the extension. For example, under thecorredor
service,volumes: [ "./corredor:/corredor/test-extension", …other volumes you might have… ]
-
Edit the
.env
(CORREDOR_EXT_SEARCH_PATHS
variable) file to register the new extension. For exampleCORREDOR_EXT_SEARCH_PATHS=/extensions:/extensions/*:/corredor/test-extension
. -
Reload the configurations (
docker-compose up -d
).
At the end, your file structure should look like this:
data/
docker-compose.yml
.env
corredor/
test-extension/
server-scripts/
/...
client-scripts/
/...
|
You can use |
Upload using git
If you are using git and a repository, we suggest you use that. Clone the repository onto your server (into the volume mentioned above). You can then pull the changes whenever the source code changes.
If it is a private repository, make sure that your git client on your server has access to it. |
Upload manually
You can use scp
, rsync
, or any other client.
Upload the extension’s source into the volume mentioned above.
I usually use |
An example
|
Debugging
DevNote: Add some insight in debugging failing scripts. |
Why is this script not valid?
-
it is defined in a
.js
file, -
it is located under
client-scripts
orserver-scripts
, -
it defines an
export default {…}
, -
defines at least one valid trigger,
-
defines a security context if it the script is a sink or deferred,
-
conforms to the script signature.
In the case of a client-script, make sure that the file structure is appropriate. |
Why can’t I see my scripts?
-
that the Corredor container has access to the extension,
-
either via an existing volume, or a new volume.
-
-
that you’ve uploaded your source files to your server,
-
that you’ve reloaded your containers.
If you’re registering a new volume, you must use If you’re using an existing volume, you can use |