Packages features
The package information must be stored in a git-hub repo and the file structure 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. Dependencies can be fo type:
- Package: This type will import functionality from other packages that are required for this package to work. For example: the git-hub package will have the package oauth as a dependency. This will avoid the developer to reimplement the oauth flow.
- Service: This type means that the in order to install a package, the app will need to have a specific service enabled. The most common case is to have the
http
service as a dependency.
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 originate from the package and are sent to the app, which can process them through listeners. For a more detailed description of how to process events from packages, refer to the documentation 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 some settings needed by the package like an API key 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.
Configuration builder
You need to pass the path of a JavaScript file that contains the configuration builder script.
In cases where dependencies on other packages require configuration, you can pass the configuration from the parent package to the child package using this script. This allows app developers to configure packages in one central location. For instance, if you’re developing a package for
Google Contacts that relies on the oauth
package as a dependency, you can configure the oauthCallback
property in the Google Contacts package. With the configuration builder, you can then pass that value to the
oauth` package imported as a dependency.
The script in this file must contain a function called configurationBuilder
with a parameter config
that should be returned. Within this function’s parameter, you will have access to the parent configuration, allowing you to configure the child dependency. In the following example, we demonstrate configuring the oauth
package, which serves as a dependency for our package.
let configurationBuilder = function (config) {
config.oauth = {
id: 'installationInfo-GooglContacts-User-'+sys.context.getCurrentUserRecord().id(),
authUrl: config.authUrl,
accessTokenUrl: config.accessTokenUrl,
clientId: config.clientId,
clientSecret: config.clientSecret,
oauthCallback: config.oauthCallback
}
return config;
}
In this script, you can utilize Slingr’s JavaScript methods and access the application’s context. Additionally, you have the flexibility to manipulate values from parent configurations before setting the children’s configurations. In the provided example, the ID of the oauth package is used to store encrypted user tokens in data stores.
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.
Package 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})
Trigger package events
The way of triggering package events is using the method sys.events.triggerEvent({pkgName}:{eventName})
. You will see more useful examples ahed but here there is a simple case:
You received a webhook from github in the http service and you want to do some refactoring on the received data and then send the event to the app.
sys.events.triggerEvent('github:webhook', {
eventName: "reopened",
type: "pullRequest",
data: data
});
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 folders that match the ones in the descriptor. There must be one folder per flow step. 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.
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.
Ui services
Ui services are useful to put custom code client side enhancing its functionalities. A good example of this are the pop up window that is opened in the oauth authentication.
Ui services should be defined in folders that match the ones in the descriptor. There must be one folder per ui service. For each flow step defined you should provide 3 files for it:
uiService.js
: this file should contain the scripts that will be loaded in the client side.uiService.json
: this file should will describe the characteristics of the ui service.
uiService.json
This file will describe the ui service and set it’s configuration. The structure of the json is:
- name: string.
- places: list of strings. Where the ui service will be loadead. Options are:
APP
,LOGIN
. - dependencies: list of jsons. Where you can add dependencies to external sources. Each dependency should contain:
- file: string. Pointing to external source
- placement: string. Options:
HEAD
,BOTTOM
- places: list of strings. Options:
APP
.LOGIN
- functions: the methods that are exposed to be called from the app.
- label: string
- name: string
- callbacks: list of strings.
- events: list of jsons. Events that can be triggered by the ui service and be sent to the app
- label
- name
uiService.js
This file will contain the scripts to be run in the client side. Methods that want to be called from the app should be added to an object called services
.
For example:
service.testFunction = function (message) {
var config = message.config;
console.log('test function arrived ',message);
service.callback(message, 'callbackEvent', config);
}
The method testFunction from the slingr application with this script:
sys.ui.sendMessage({
scope: 'uiService:{descriptiorPkgName}.{uiServiceName}',
name: 'testFunction',
config: {
data: "event data"
},
callbacks: {
callbackEvent: function (originalMessage, callbackData) {
//do something
}
}
});
For more information on how ui services work you can see ui services.