Packages structure in the repo must be:
- Descriptor file
- Name
- Dependencies
- Events
- Configuration
- Configuration Builder
- Metadata
- Scripts
- Listeners
- Flow steps
- Ui services
Descriptor file
The descriptor file should be located at the root of your project and has the name package.json
. It contains
the definition of your package, like name, functions, events, configuration, etc.
Name
This is the internal name of the package and will be used to determine the type of package. Usually in lower case with dashes.
Dependencies
...
"dependencies": [
{
"type": "service",
"name": "http",
"version": "v1.2.6"
},
{
"type": "package",
"name": "oauth",
"version": "^v1.0.15"
}
],
...
The dependencies
section allows to set dependencies that the package needs to work.
These are the settings required for each dependency:
type
: Dependencies can be of type service or package.name
: The name of the dependency.version
: This have the format vX.Y.Z. i.e: v1.0.15. To set the behaviour of the update policies related to dependencies prefixes can be added to the version. These prefixes can be:- ^ : Compatible version. This will listen to updates as long as the X value doesn’t change. i.e. 1.Y.Z
- ~ : Bug fixes. This will listen to updates as long as the Y value doesn’t change. i.e. 1.2.Z
- No prefix: Version is fixed.
Events
...
"events": [
{
"label": "Inbound Event",
"name": "inboundEvent",
"description": "Event triggered by the package when a request arrive to the root path of the service."
},
{
"label": "Pong",
"name": "pong",
"description": "Callback event for the Ping function"
},
...
],
...
Events are originated on the package and sent to the app that can process them through listeners. A more detailed description of how to process events from packages in found here.
The definition of the events just contains a few properties. The content of the event is defined by the package and the app will receive it as a JSON, but no need to define the structure here.
These are the properties of events:
label
: this is the name developers will see in the app builder.name
: this is the internal name of the event that will be used to identify it in the code.description
: a brief description about what this event is and when it is triggered.
Configuration
The package can be configured in the app builder. Usually, developers will need to enter API keys and other settings needed by the package in order to work.
This is how the configurations are defined at the root of the descriptor:
...
"configuration":[
...
]
Basically each configuration is described by a set of fields, which have the following structure:
{
"label": "Field",
"name": "field",
"type": "password",
"multiplicity": "one",
"required": true,
"visibility": @config.otherField,
"showLabel": true,
"defaultValue": "12345678",
"typeOptions": {}
}
Here is a brief description of each property:
label
: this is the label that will be shown on the left of the field. It is required ifshowLabel
is set totrue
(default value).name
: represents the key under value will be stored in configuration. This key is also accessible throughconfig
global variable.type
: the type of the field. This will determine thetypeOptions
field. More information about types below.multiplicity
: there are two possible values:one
(default value) ormany
. The second value will allow to define multiple values for this field (it will be an array).required
: indicates that the field is mandatory. It can be represented with a booleantrue
orfalse
, a reference like@config.otherField
or and expression asconfig.otherField && utils.isPlaceHolder(config.anotherValue)
visibility
: indicates if the field is visible in UI. It can be represented with a booleantrue
orfalse
, a reference like@config.otherField
or and expression asconfig.otherField && utils.isPlaceHolder(config.anotherValue)
showLabel
: iffalse
, the label will not be displayed and the field will use all the width available. It is represented with a booleantrue
orfalse
.defaultValue
: value set by default if a value has not be set by the user.typeOptions
: only valid for some types, check next section.
Next we will describe the available types:
Label
Type: label
A label represents a read only value that will not be stored as part of configuration. It only represents information for user or developer.
{
"label": "Simple",
"name": "simpleLabel",
"type": "label",
"value": "Sample Complex Package"
}
This field type has no options.
Information
Type: info
Allows to display an alert where HTML code can be inserted.
{
"label": "Information",
"name": "information",
"type": "info",
"typeOptions": {
"alertType": "warning"
},
"value": "Follow these points to generate a new credentials:<ul><li>Access to the Dev Console</li><li>Create a new project. Copy the 'Project Name' in the configuration form.</li><li>...</li></ul>"
}
Options:
alertType
: describes the color or format to display it. Valid values are:info
,success
,warning
anddanger
.
Fields group
Type: fieldsGroup
This is a special kind of field and allows to nest other fields inside it.
{
"label": "Labels",
"name": "labels",
"type": "fieldsGroup",
"typeOptions": {
"fields":[
{
"label": "Simple",
"name": "simpleLabel",
"type": "label",
"value": "Sample Complex Package"
},
{
"label": "Concatenation",
"name": "concatenation",
"type": "label",
"value": "'Prefix ['+(config.entity ? config.entity : 'No entity') +'] > ['+(config.entityField ? config.entityField : 'No field')+'] > ['+(config.entityAction ? config.entityAction : 'No action')+']'"
},
{
"label": "Multi",
"name": "multiLabel",
"type": "label",
"multiplicity": "many",
"value": [
"config.entity ? config.entity : 'No entity'",
"config.entityField ? config.entityField : 'No field'",
"config.entityAction ? config.entityAction : 'No action'"
]
}
]
}
}
Options:
fields
: must respect the same structure thanconfiguration
oruserConfiguration
, which means it is basically an array of fields. Any number of levels are supported but not recommended more than two levels due to UI available space.
Text
Type: text
This is a common text input, the value will be stored as a simple string. It can have validations set through type options
{
"label": "Email",
"name": "email",
"type": "text",
"required": true,
"typeOptions": {
"validation": "email"
}
}
{
"label": "Description",
"name": "description",
"type": "text",
"typeOptions": {
"representation": "textArea",
"numberOfRows": 4
}
}
Options:
validation
: validations available are:email
,number
andurl
. All these validations allow placeholders.representation
: the component to be used to represent the field. Valid options are:inputText
(default) andtextArea
.numberOfRows
: in the case that the selected representation istextArea
it is possible to set the number of rows to be displayed.
Password
Type: password
Represented by a password input, the value will not be shown to the developer, however it will be stored as a simple string. Keep in mind that all settings of packages are encrypted as usually there is sensitive information.
{
"label": "Password",
"name": "password",
"type": "password",
"required": true
}
This field type has no options.
Toggle
Type: toggle
Creates a toggle widget and will store a boolean value.
{
"label": "Sync Automatically",
"name": "syncAutomatically",
"multiplicity": "one",
"type": "toggle"
}
This field type has no options.
Button
Type: button
This component allows the execution of an action when clicked. No information is stored as value of this field. This
component does not allow multiplicity
equals to many
.
{
"label": "Set email",
"name": "setEmail",
"type": "button",
"typeOptions": {
"color": "info",
"action": "if (!config.inputs.email) { config.inputs.email = 'test1@slingr.io'; }"
}
}
Options:
color
: available values areinfo
,default
,primary
,success
,warning
anddanger
.action
: this is an expression that will be parsed and executed as Javascript function on client side. Some interesting example can be found in the official packages. For example the Google Calendar package uses buttons to trigger the OAuth process.
Buttons group
Type: buttonsGroup
This component creates a group of buttons. If multiplicity
is set to true
, many values could be selected.
{
"label": "Multi",
"name": "multiSwitcher",
"type": "buttonsGroup",
"multiplicity": "many",
"required": true,
"defaultValue": ["danger", "info"],
"typeOptions": {
"possibleValues":[
{
"label":"Danger",
"name":"danger"
},
{
"label":"Warning",
"name":"warn"
},
{
"label":"Information",
"name":"info"
}
],
"allowCustom": true
}
}
Options:
possibleValues
: this is an array of elements withlabel
(text shown as options) andname
(string stored as value). These will be the options.allowCustom
: indicates that a placeholder can be set as value, creating an input next right to buttons to select it. This is important if the value of this field might need to change between different environments of your app.
Drop down
Type: dropDown
This component creates a combo-box. In this case, if multiplicity
is many
, more than one value can be selected.
{
"label": "Multi Custom",
"name": "multiCustomDropDown",
"type": "dropDown",
"multiplicity": "many",
"defaultValue": ["${TEST2}", "CA"],
"typeOptions": {
"allowCustom": true,
"possibleValues":[
{
"label":"New York",
"name":"NY"
},
{
"label":"Arizona",
"name":"AZ"
},
{
"label":"California",
"name":"CA"
}
]
}
}
Options:
possibleValues
: this is an array of elements withlabel
(text shown as options) andname
(string stored as value). These will be the options.allowCustom
: indicates that a placeholder can be set as value, creating an input next right to buttons to select it. This is important if the value of this field might need to change between different environments of your app.
Metadata
In the descriptor file we must define the metadata we want to include in the package. The structure would be:
...
"metadata": [
{
"type": "script",
"path": "/scripts/functions.js"
},
{
"type": "listener",
"path": "/listeners/listeners.js"
}
]
...
These are the properties of metadata:
type
: the type can be script, listener, uiService, flowStep.path
: the path to find the required files. The path can point to a folder or a file depending on the metadata.
Scripts libraries
Javascript files are imported to the app and each file represents a script library. Exposed variables should be included in exports as it is done for app libraries.
Scripts should be put in the scripts
folder of your package and the names of the files need to match the ones in
the descriptor.
This scripts provided by the package are executed in the context of the app as any other script library. These libraries and methods can be called from anywhere in the app.
A script file will look like this:
var s = function(a, b){
return a+b;
};
exports.sum = s;
exports.rnd = function(){
return Math.random();
};
exports.PI_VALUE = Math.PI;
exports.rndSum = function(){
return this.sum(this.rnd(), this.PI_VALUE);
};
exports.getAccessToken = function () {
return dependencies.oauth.functions.connectUser(); // using package dependency
}
exports.sites.permissions.post = function (sitesId, httpOptions) {
if (!sitesId) {
sys.logs.error('Invalid argument received. This helper should receive the following parameters as non-empty strings: [sitesId].');
return;
}
var url = parse('/v1.0/sites/:sitesId/permissions', [sitesId]);
sys.logs.debug('[pkgName] POST to: ' + url);
let pkgConfig = config.get(); // getting package configuration
sys.logs.debug('[pkgName] config: '+JSON.stringify(pkgConfig));
var options = checkHttpOptions(url, httpOptions);
options.authorization = mergeJSON(authorization, {
type: "oauth2",
accessToken: pkgConfig.token,
headerPrefix: "Bearer"
});
return dependencies.http.post(options); // using service dependency
};
Package config
The values of the package configuration can be accessed within the js files like it is shown next:
config.get(); // returns configuration map
config.get(parameterName);// returns parameter value of the configuration
Usage of package dependencies
Package dependencies can be accessed this way:
// for dependencies on packages: dependencies.<depName>.<library>.<function>
dependencies.oauth.functions.connectUser();
// for dependencies on services: dependencies.<depName>
dependencies.http.post()
Stores
If your package needs to store data persistently, you will need to use the app storage. In this key value storage you can save JSON documents, find them by keys, update or remove them. Elements can be stored encrypted:
// store a user token
sys.storage.put(config.id + ' - access_token', response.access_token, {encrypt: true});
// get a user token
sys.storage.get(config.id + ' - access_token', {decrypt: true})
Ui services
Ui services are useful to put custom code client side enhancing its functionalities. You can check the ui services docs In the package repo it consists of a folder with the ui service name containing two files: uiService.js and ui service.json.
The ui service json is a descriptor file that works similar to the way the package.json file do. The structure is as follows:
- Name
- Places: App, login
- Dependencies:
- File: external source or file
- Placement: head, bottom
- Places: App, login
- Functions:
- Label
- Name
- Callbacks(optional)
- Events:
- label
- name
The uiService.js is the code that will be runned client side. To check how this file works check the ui services docs.
Listeners
You need to define listeners in a js file.
In this scripts you will be able to define listeners dynamically. This way, when developers add the package to their apps, listeners will be automatically appended.
For example, you could use this feature to process a webhook sent to a service related to the package.
Listeners should be added as properties to the object listeners
:
listeners.defaultWebhookSharepoint = {
label: 'Catch HTTP sharepoint events',
type: 'service',
options: {
service: 'http',
event: 'webhook',
matching: {
path: '/sharepoint',
}
},
callback: function(event) {
sys.logs.info('Received Sharepoint webhook. Processing and triggering a package event.');
var body = JSON.stringify(event.data.body);
var params = event.data.parameters;
if(true) {
sys.logs.info('Valid webhook received. Triggering event.');
sys.events.triggerEvent('sharepoint:webhook', {
body: body,
params: params
});
return "ok";
}
else throw new Error("Invalid webhook");
}
};
The name of the variable will be the name of listener, in addition the user must configure the label
of the listener, then type
(it can be data
, service
and job
). The options
field represents the proper configuration for the type. Finally, the callback
is the action executed by the listener.
The header of this function is ignored, only the inner code is used, developers must consider the parameters coming based
on listener type. See documentation
Next we will briefly describe the different types of listeners:
Data
See the example:
listeners.listenerForCompaniesChanges = { //The name will be taken from this namespace
label: 'Listener for Companies changes', //label configuration
type: 'data',
options: {
executeInBackground: true, //Indicates if the listener must be executed in background
entity: 'companies', //The name or id of the entity listened
events: [ //entity events configuration
{type: 'recordCreated'}, //the type of entity event, it can be 'recordCreated', 'recordChanged', 'recordDeleted', 'actionPerformed'
{type: 'actionPerformed', action: 'assignCompanyType'} //not all event types are available, in addition, for 'actionPerformed' the name or id can be specify
]
},
callback: function(event, record, oldRecord) {// available parameters are 'event', 'record' and 'oldRecord'
sys.logs.info('Entering to listener handler'); //JS API functions are available
sys.logs.info('Event: '+JSON.stringify(event));
sys.logs.info('Record: '+JSON.stringify(record));
sys.logs.info('Old record: '+(oldRecord ? JSON.stringify(oldRecord) : ''));
}
};
Services
See the example:
listeners.defaultWebhookSharepoint = {
label: 'Catch HTTP sharepoint events',
type: 'service',
options: {
service: 'http',
event: 'webhook',
matching: {
path: '/sharepoint',
}
},
callback: function(event) {
sys.logs.info('Received Sharepoint webhook. Processing and triggering a package event.');
var body = JSON.stringify(event.data.body);
var params = event.data.parameters;
if(true) {
sys.logs.info('Valid webhook received. Triggering event.');
sys.events.triggerEvent('sharepoint:webhook', {
body: body,
params: params
});
return "ok";
}
else throw new Error("Invalid webhook");
}
};
Job
See the example:
listeners.listenerForExportRecords = { //The name will be taken from this namespace
label: 'Listener for Export records', //label configuration
type: 'job',
options: {
jobType: 'exportRecords',//job type, available are: `startApp`, `stopApp`, `importRecords`, `exportRecords`, `importUsers`, `exportUsers`
event: 'finished'// status listened, available are: `created`, `started`, `finished`, `stopped`, `resumed`, and `canceled`
},
callback: function(event) {// only `event` is a available parameter for this function
sys.logs.info('Entering to listener handler'); //JS API functions are available
sys.logs.info('Event: '+JSON.stringify(event));
}
};
Flow steps
You can create customized flow steps for your package.
Flow steps should be defined in the flowSteps
folder of your package and the names of the files need to match the ones in the descriptor.
For each flow step defined you should provide 3 files for it:
icon.png
: this is the icon for the step in the flow editor. The icon requires to have the PNG format and, be of size 18x18px.step.js
: this file should contain the function that will be executed as part of the step. This function must receive a unique object parameter called ‘inputs’ containing the function parameters.step.json
: this file should contain the name of the step along with inputs and outputs for it.
Once you’ve created the required files you’ll need to register your package with a new version. More information can be found at: Package Registration
After registering the new package version and setting that new version from the developer portal you will be able to see the new flow steps available at the flow editor.
Required files examples
Examples for step.js
file:
/**
* Generates a random number.
*
* @param {object} inputs {number} bound, This is used to get a random number between 0 (inclusive) and the number
* passed in this argument, exclusive.
*/
step.numberGenerator = function (inputs) {
var data = pkg.utils.functions.randomNumber({bound: inputs.bound});
return {
"generatedNumber": data['number']
};
};
Examples for step.json
file:
{
"label": "Random numb",
"name": "numberGenerator",
"category": "integrations",
"description": "Generates a new random number.",
"inputs": [
{
"label": "Bound",
"name": "bound",
"type": "text",
"description": "This is used to get a random number between 0 (inclusive) and the number passed in this argument, exclusive",
"required": "true",
"defaultValue": 2000
}
],
"outputs": [
{
"label": "Generated number",
"name": "generatedNumber",
"type": "number",
"description": "The generated random number"
}
]
}
Inputs examples:
"inputs": [
{
"label": "Operation",
"name": "operation",
"type": "choice",
"defaultValue": "SUM",
"required": "true",
"options": {
"possibleValues": [
{
"label": "Sum",
"name": "SUM"
},
{
"label": "Rest",
"name": "REST"
},
{
"label": "Division",
"name": "DIV"
},
{
"label": "Multiplication",
"name": "MULT"
}
],
"allowContextSelector": "false"
}
},
{
"label": "First operand",
"name": "firstOperand",
"type": "text",
"description": "First operand",
"required": "true"
},
{
"label": "Second operand",
"name": "secondOperand",
"type": "text",
"description": "Second operand",
"required": "true"
}
]
Output examples:
"outputs": [
{
"label": "result",
"name": "result",
"type": "object",
"description": "Result object containing temperature, pressure and humidity for the given city"
}
]
"outputs": [
{
"label": "city",
"name": "city",
"type": "text",
"description": "name of the city"
},
{
"label": "temperature",
"name": "temperature",
"type": "number",
"description": "temperature for the given city"
},
{
"label": "pressure",
"name": "pressure",
"type": "number",
"description": "pressure for the given city"
},
{
"label": "humidity",
"name": "humidity",
"type": "number",
"description": "humidity for the given city"
}
]
For more information about flows you can go to Developer’s reference: Flows Overview