Skip to content

Instantly share code, notes, and snippets.

@amaralli
Last active February 21, 2017 00:01
Show Gist options
  • Save amaralli/ee61264e01af5e95e001e5bb10df54f1 to your computer and use it in GitHub Desktop.
Save amaralli/ee61264e01af5e95e001e5bb10df54f1 to your computer and use it in GitHub Desktop.
Forge Documentation- How to understand and utilize the Forge Connector Creator tool

#Getting Started ##What is a Connector? A Connector is an interface that communicates with external APIs. The goal of a Connector is not to be a direct reflection of an API, but instead a user-friendly abstraction on top of an API. Connector development is more focused on end-user needs than on the capabilities of the API. Each Connector is made up of methods, which appear to the user as the different Event and Action cards. Each method determines how data is fetched from the API, and transforms data to the user-friendly format accepted by the front-end. Most methods use more than one API call to do this.

To define a Connector, you must write a Connector JSON file and submit this file to Azuqua for upload into the engine. At runtime, the engine will access the instructions laid out in this file to execute the Event or Action the user has designated in their FLO. The primary function of a Connector JSON file is to lay out in a linear manner the pre-defined action steps (known as modules) that will execute each user-facing event or action. Using modules, you can define what data will be fetched from the API, select the data you want to pass on to the user, and transform this data into flat JSON that can be consumed by the Azuqua front-end.

Each Connector has 5 sections:

  • The General section, where you define general information about the Connector, such as the display name of the Connector, and the version number.
  • The Authentication section, where you define the Authentication schema of the Connector. Azuqua supports 3 authentication schemes: basic, OAuth (both 1 and 2) and custom. Once users have set up an authentication configuration for a Connector, Azuqua will save their credentials so this configuration can be used again.
  • The Events section, where you define any Event cards you want in your Connector. Events are the methods that start FLOs. Each FLO must have a starting Event, and every time the Event detects new records the FLO will run.
  • The Actions section, where you define any Action cards you want in your Connector. Actions are the methods that make up the majority of a FLO, and execute whenever the starting Event occurs.
  • The Metadata section, where you define any helper methods you need to execute your Events and Actions. While Events and Actions are turned into cards that are visible to the user, helper methods are never seen by the user and always execute under the hood.

##API Requirements To add your application as an Azuqua Connector, your API must be able to give Azuqua enough insight into your application to start FLOs whenever important changes happen. The most efficient way to do this is through webhook notifications, but Azuqua can also monitor for changes by polling your API for updates on an incremental basis determined by the user's Azuqua plan.

Additionally, Azuqua needs to be able to replicate common user actions through your API. For example, if users can copy items or link contacts to accounts inside your app, your API should allow Azuqua to do that as well. Read on for more specific requirements.

###All applications:

  1. Your API must use one of the four supported authentication schemes: basic, key, OAuth 1.0, and OAuth 2.0. Session-based authentication is not supported, since users need to be able to set up authentications and let them run instead of regularly re-authenticating.
  2. Your API must support field-specific queries of your resources. For example, if your application has a contact resource, the API should allow developers to query for a contact by name and email.
  3. It is best if your API timestamps its records in a standard format (such as Unix/Epoch or ISO8601).

###If your API supports webhooks:

  1. Developers should be able to programmatically set up and tear down webhooks through the API.
  2. All users should have permissions to create webhooks through the API, but the data that triggers the webhook should be scoped to the data that the authenticating user has access to (users should not be able to set up a webhook that monitors resources they can't access).
  3. There should be a webhook notification for each time a resource is created, modified, or deleted. These notifications should be granular enough that the developer can focus on changes that involve a specific type of resource (but not only a single resource).
  4. The webhook that notifies Azuqua when a certain resource has been modified should also have information about what exactly was changed. For example, if there is a webhook notification for whenever a resource of a certain type is modified, the webhook should include information about which attributes were modified and what the new values of those attributes are.

###If your API does not support webhooks:

  1. Developers should be able to query a feed of all the updates that have happened in the authenticated instance of the application, including whenever a record is created, modified, or deleted.
  2. If possible, each update in this feed should have a unique, incrementing ID. When the developers are querying the API, they should be able to pass the ID of the last update they looked at and receive all of the updates that have happened since then. It is also possible to use a timestamp instead of an ID to query for fresh records, but timestamps are not always specific enough and may result in duplicate FLO executions for a single record.
  3. Developers should be able to filter their query by the type of resource they wish to monitor.
  4. Developers should be able to filter their query by the exact type of update that has occurred (e.g. record deleted, name updated, assigned to member, record parent changed, etc.).
  5. If the update that has occurred in the application involves the value of a field being changed, the API should indicate what field was changed and what the new value is.

###If your application supports custom resource types:

  1. Developers must be able to query your API for a list of all resource types.
  2. Developers must be able to query your API for the "shape" of each resource type, and get back a list of fields that is possible for the user to edit.

###If your application supports custom fields:

  1. Developers must be able to query your API for a list of the custom fields associated with the authenticated instance of the application.
  2. The custom fields should have both a unique identifier and a user-friendly name. It is possible for these two properties to be the same if custom field names are enforced as unique in your application.

##Designing your Connector ###Determining Your Core "Resources" At its core, any application deals with the creation or modification of "resources." Users typically don't think of applications this way. When they are building a new task list, or adding a contact to their address book, they don't think of tasks and contacts as different kinds of "resources" that the application is creating for them.

However, APIs are frequently organized around "resources," with methods built around each of the resource types the application handles. The first step in building a Connector is figuring out which resources you want your Connector to monitor and modify (these are not always the same). For example, you may want your users to be able to monitor for new customers in your application, but not want them to create customers themselves since it involves handling sensitive information.

Your Connector does not have to include every kind of resource your application works with. Think instead about the resources your users directly interact with the most, and start with those. It's better to start with a small Connector and develop to accommodate more use cases, then to build a large but confusing Connector off a set of assumptions about what users need.

###Designing your Events and Actions Most Connectors include at least two Events and three Actions for every resource type. These five basic cards are:

  • New Resource (Event): this card will trigger a FLO whenever a new resource of this type is created. Resource Updated (Event): this card will trigger a FLO whenever any resource of this type is updated.
  • Create Resource (Action): this card will create a new resource of this type as part of a FLO.
  • Read Resource (Action): this card will take in the unique identifier of a resource (frequently an ID or URL) and return information about it as part of a FLO.
  • Update Resource (Action): this card will take in the unique identifier of a resource (frequently an ID or URL) and update the resource's information as part of a FLO.

Additionally, many Connectors also include three other cards when the API supports these Actions and the user scenarios require them:

  • Deleted Resource (Event): this card will trigger a FLO whenever any resource of this type is updated.
  • Search Resources (Action): this card will take information about a resource and search for the unique identifier of a resource with matching information. Frequently, it is used in conjunction with a "Read Resource" card so users can look up a record using information they possess and get other information about it. For example, if a user knows the email of a contact but wants to get the contact's first name and last name, they might use a "Search Contacts" card to find the ID of the contact resource that matches the email, then drag that ID to a "Read Contact" card to find the first name and the last name.
  • Delete Resource (Action): this card will take in the unique identifier of a resource (frequently an ID or URL) and delete the resource as part of a FLO. For this reason, this kind of card is more rare, since it makes it possible for users to accidentally permanently delete records.

Although these guidelines provide an outline for your Connector, the best Connectors include cards that allow users to mimic common actions they take in your app. For example, if moving resources on and off lists is a common action in your app, you might want to have "Add Resource to List" and "Remove Resource from List" Actions in your Connector.

###Choosing Your Fields Often, the fields you put on your cards are determined by the limitations of your application's API. However, you should make decisions about which fields the users need and don't need. It is often helpful to have an idea of the fields you want your card to take in and return before you start building, to at least provide guidance.

As a general rule of thumb, most Events include these fields when possible:

  • Timestamps (time created, time updated, etc.)
  • Information about the user who created the resource
  • Information about the user assigned to the resource
  • Information about the user who updated the resource (in the case of an "Updated" or "Deleted" Event)
  • The unique identifier of the resource
  • The URL of the resource
  • Any basic information about the resource that the user will be able to see in the UI (such as title, parent resource title, description, etc.)

Actions are harder to generalize, since depending on what your Action does, it could have very different inputs and outputs from other Actions. However, Actions must have "symmetry," meaning that if one Action requires a unique ID to update or delete a resource, other Actions should provide that ID--when you create a record, you get back the ID you'll need to update it later.

##Starting your connector After you've laid out the general shape of the Connector, it's time to start a new Connector project. To start a new project, go to the dropdown menu in the left-hand corner of the toolbar, and select "New Channel" ('Channel' is an older term for a Connector) . The Connector Builder will open a new Connector file in your workspace, and take you to the "General" section.

In this section of the Connector, you should fill out certain basic information, including:

  • name, the display name of your Connector that will appear in the Azuqua UI. This can be changed at any time.
  • description, the description of your Connector. This will not appear in the UI, but it may be used for internal purposes.
  • version, the current version number of the Connector. Read more about Version Control here.
  • type, must be set to "channel"
  • recurrence, must be set to 1

Now, save your Connector by clicking the "Save" button in the right-hand side of the tool bar. The first time you save a Connector, you will be asked to enter a unique draft name for your Connector. This is not your display name--this name will not appear to the user, but it will identify your Connector to the Azuqua engine, so it must be unique. If you have filled in a display name already, the Azuqua Connector Builder will suggest a name for you. Normally, the draft name is just the name of your application, one word, lowercase. Once you hit "Submit," your Connector will be saved and you will now see the unique draft name in your Connectors dropdown.

##Version Control Azuqua uses Semantic Versioning to version Connectors. You have three numbers: a major version number, a minor version number, and a patch number. Each number is separated by decimals, and they provide information about the magnitude of the change made in each version.

For example, the first time you build a Connector, you might submit version number 1.0.0. Then, you need to make a patch to fix a bug, so you submit version 1.0.1 and 1.0.2 as you debug your work. Later on, you decide to add a card. Since this is a bigger change but it's still backward-compatible, you release version 1.1.0. Then in a few months, your application gets a new version of the Connector and it needs a complete rebuild. This will be released as version 2.0.0 and so on. How your team uses version numbers to track changes is up to you. Currently, the Azuqua Engine does not pay any attention to version number, it only runs FLOs on the most recently published version of the Connector.

While you are working on a Connector, the version you are changing is kept as 'DRAFT', and can be accessed from the bottom of the version tab by the Connector name. Every time you save edits they over-write the most recent version and save to 'DRAFT'. To capture a version of a Connector, increment the version number and hit "Submit." This will build the current draft of the Connector and save it in a separate location, so it can't be overwritten. It will now appear in the Control Panel so an administrator can deploy it to an environment.

#Authentication ##General Authentication In the Azuqua designer, users can connect an account to a Connector, set up their FLO, and then let it run. Azuqua handles all the hard work of authentication, and securely stores any pertinent authentication information (like credentials, API keys, or access keys obtained from an OAuth exchange) so it can be used whenever the FLO runs.

Azuqua supports 4 kinds of authentication: Basic, OAuth 1.0, OAuth 2.0, and Custom (often used for Key/Secret). In Custom authentication, you also may choose to define an authentication object that takes in custom parameters that you can then send over as part of your request to the API.

To set up authentication, go the Authentication section on the sidebar, and select the authentication type your API requires. Each form has slight variations, but all authentication types come in the basic form:

{ 
	"type": "" 
	"authparams" : { 
		"parameterName": {
			"type": "", 
			"displayname" : "" 
		} 
	} 
} 

Fields:

  • "type": The type of authentication (will be automatically populated)
  • "authparams": The set of parameters you would like to show up in the authentication visual elements of your Connector.
  • "parameterName": An individual authentication parameter that will be on your Connector. This name will not be surfaced onto the Connector itself, but will be the key you use to refer to the value throughout your methods. You may have as many of these as you need.
    • "type": The type of this parameter. Can be string, or password (hides the inputted value on the UI)
    • "displayName": The name that will be shown to the user on your Connector in Azuqua's Designer.

Although Azuqua will handle the storage of authentication information, many APIs require you to use this authentication information to sign requests. To do this, all authentication parameters can be referred to in other methods using Mustache, in the form: "{{auth.parameterName}}".

##Setting Up Basic Authentication Use basic authentication when all you need to grant access to the API is a username and password, sent over in the headers or body of the request.

Template:

{ 
  "type": "basic", 
  "authparams": { 
    "username": { 
      "type": "string", 
      "displayname": "Username" 
    }, 
    "password": { 
      "type": "password", 
      "displayname": "Password" 
    } 
  } 
} 

Fields:

  • "type": must be set to 'basic' for basic authentication
  • "authparams": an object that contains the authentication parameters users will enter. For Basic auth, these are usually "username" and "password"
    • "type": can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
    • "displayname": the name that will display above the authentication parameter in the UI

Example:

{ 
    "type": "basic", 
    "authparams": { 
        "username": { 
            "type": "string", 
            "displayname": "Username" 
        }, 
        "password": { 
            "type": "password", 
            "displayname": "Password" 
        }, 
        "instance_url": { 
            "type": "string", 
            "displayname": "Instance URL (w/o https)" 
        } 
    } 
}

Later on, you can reference these parameters using the Mustache templates {{auth.username}} and {{auth.password}}.

##Setting Up OAuth 1.0 Template:

{ 
  "type": "oauth", 
  "version": "1.0", 
  "request_url": "", 
  "access_url": "", 
  "consumer_key": "", 
  "consumer_secret": "", 
  "callback": "{server}/app/oauth/(YOUR APP)/authorize", 
  "signature_method": "HMAC-SHA1", 
  "nonce_size": null, 
  "redirect": "" 
} 

Fields:

  • "type": must be set to 'oauth'
  • "version": must be set to 1.0 for OAuth 1
  • "request_url": the external URL where the token request will be made
  • "access_url": the external URL where the request token will be exchanged for the access token
  • "consumer_key": the access key obtained from the application
  • "consumer_secret": the access secret obtained from the application
  • "callback": the callback URL. Substitute the Connector's unique filename (the one you created when you first saved your Connector) for "(YOUR APP)"
  • "signature_method": must be 'HMAC-SHA1'
  • "nonce_size": must be null
  • "redirect": the redirect URL (may also be called the authorize URL).

Optional Fields:

  • "authparams": an object that contains the authentication parameters users will enter. Only include authparams in the OAuth object if you require users to enter additional data before you can run the OAuth exchange, such as an instance URL. Each parameter defined in this object can be referenced as {{auth.(parameter key)}} using Mustache.
    • "type": can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
    • "displayname": the name that will display above the authentication parameter in the UI

The access token that results from the OAuth exchange can be referenced from inside each method and hashed into HTTP calls using the Mustache template {{auth.access_token}}. After you have set up your authentication, all requests to the API will be automatically signed for you according to OAuth 1.0 protocol.

OAuth 2.0 Redirect URL Some applications require you to set acceptable callback or redirect URLs when you register your application. You must add this redirect URL to your application to be able to run OAuth 1.0 from the Connector Builder, Beta Environment, and Production Environment:

https://betaapi.azuqua.com:443/app/oauth/(FILENAME)/callback 

Substitute the Connector's unique filename (the one you created when you first saved your Connector) for "FILENAME".

##Setting Up OAuth 2.0 Template:

{ 
  "type": "oauth", 
  "version": "2.0", 
  "base_site": "", 
  "authorize_path": "", 
  "access_token_path": "", 
  "access_token_name": "access_token", 
  "refresh_token_name": "", 
  "client_id": "", 
  "client_secret": "", 
  "appParams": { 
    "one": { 
      "state": true, 
      "redirect_uri": "{server}/app/oauth/(YOUR APP)/authorize" 
    }, 
    "two": { 
      "redirect_uri": "{server}/app/oauth/(YOUR APP)/authorize" 
    }, 
    "refresh": { 
      "refresh_token": "" 
    } 
  } 
} 

Fields:

  • "type": must be set to 'oauth' for OAuth 2
  • "version": must be set to 2.0 for OAuth 2
  • "base_site": the base URL
  • "authorize_path": the URL (in addition to base_site) where users will be directed to give authorization
  • "access_token_path": the URL (in addition to base_site) where the access token will be obtained
  • "access_token_name": the key of the access token in the response
  • "refresh_token_name": (optional) the key of the refresh token in the response
  • "client_id": the client ID obtained from the application. Automatically appended to both legs of your authentication.
  • "client_secret": the client secret obtained from the application. Automatically appended to both legs of your authentication.
  • "appParams": an object containing the parameters for two-leg authentication
    • "one": an object that contains the parameters required for the first leg of your authentication. The parameters included in the template are common parameters, but not all applications use the same fields.
      • "state": a boolean that if true will include an unguessable state value in the OAuth exchange
      • "client_id": the client ID provided when you registered your application
      • "redirect_uri": the URL where users will be sent after authorization. Substitute the Connector's unique filename (the one you created when you first saved your Connector) for "FILENAME"
    • "two": an object that contains the parameters required for the second leg of your authentication except for code, which is automatically passed to the second leg of the authentication exchange. The parameters included in the template are common parameters, but not all applications use the same fields.
      • "client_id": the client ID provided when you registered your application
      • "client_secret": the client secret provided when you registered your application
      • "redirect_uri": the URL where users will be sent after authorization. Substitute the Connector's unique filename (the one you created when you first saved your Connector) for "FILENAME"
  • "refresh" (optional), an object that contains the parameters required to exchange a refresh token for a newauth_token if the token expires

Optional Fields:

  • "authparams": an object that contains the authentication parameters users will enter. Only include authparams in the OAuth object if you require users to enter additional data before you can run the OAuth exchange, such as an instance URL. Each parameter defined in this object can be referenced as {{auth.(parameter key)}} using Mustache.
  • "type": can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
  • "displayname": the name that will display above the authentication parameter in the UI

The access token that results from the OAuth exchange can be referenced from inside each method and hashed into HTTP calls using the Mustache template {{auth.access_token}}.

OAuth 2.0 Redirect URL Some applications require you to set acceptable callback or redirect URLs when you register your application. You must add these redirect URLs to your application to be able to run OAuth 2.0 from the Connector Builder, Beta Environment, and Production Environment:

 https://betaapi.azuqua.com:443/app/oauth/(FILENAME)/authorize 

Substitute the Connector's unique filename (the one you created when you first saved your Connector) for "FILENAME".

##Setting Up Custom Authentication

Custom authentication allows you to define your own authentication parameters. Often, custom auth is used for key/secret authentication.

Template:

{ 
  "type": "custom", 
  "authparams": { 
    "": { 
      "type": "string", 
      "displayname": "" 
    } 
  } 
} 

Fields:

  • "type": must be set to "custom" for custom authentication
  • "authparams": an object that contains the authentication parameters users will enter. For custom auth, you can define any authparams as you like
  • "type": can be 'string' or 'password.' If type is 'password,' the input will be hidden as the user types it in
  • "displayname": the name that will display above the authentication parameter in the UI

Example:

{ 
    "type": "custom", 
    "authparams": { 
        "apikey": { 
            "type": "string", 
            "displayname": "Username" 
        }, 
        "secret": { 
            "type": "password", 
            "displayname": "Password" 
        } 
    } 
}

No matter how you define your authentication schema, you'll be able to reference your authentication parameters later on using Mustache in the form: "{{auth.parameterName}}"

#Events and Actions ##About Events and Actions Once you've set up Authentication, you can start adding Events and Actions to your Connector. Each Event and Action your Connector supports represents a method that an Azuqua user can use to interact with your API. Events are methods that monitor for a change in a system and are responsible for starting FLOs. Actions are methods that will run when the Event it's attached to is triggered, creating the actual functionality of a FLO.

All Events and Actions are composed of two parts:

  • The schema that controls the parts of your Connector that users can see and manipulate on your card (params, input, and output)
  • The underlying code that actually makes calls to the API and handles the response (modules in the Core section of your Connector).

"Events and Actions" will focus on the first, UI-centered part of creating these components of your Connector.

For information about the Modules used to power the functionality of your Events and Actions, see "Using Modules".

Both the UI and functional parts of Events and Actions are supported by metadata methods, which act as helper methods. For more information, see "Metadata Methods".

##Events You can add a new Event to your Connector by selecting Events > Add New Method. There are two types of events, depending on how your API works. There are polling events, and webhook events.

###Polling Events
####About Polling Events Polling Events are one of the two types of Events that your Connector can contain. Polling Events are designed to automatically run on a recurring basis based on the user's plan (typically 1-5 minutes). This means that these Events will need to grab a collection of records, then process collections of records and determine which records have happened since its last iteration.

To be able to keep track of the last record that was collected, Polling Events use a tool called the "since" value. This value allows your API to understand when the last query sent from your Connector was, and can take many forms, although it often is a timestamp sent in your HTTP request. When the user turns on a FLO, Azuqua will call the API to get the current since value. Then, at a specified interval, the Event will call the API again and use the since value saved during the previous run to fetch new records. The engine will automatically execute the FLO once for every new record.

####The Since Value The "since" value is a single piece of data that persists between executions of an Event. This means that the data that was set on a previous run of an Event can be compared against a current run's dataset. Usually the "since" value is a timestamp or the ID of the last record collected of a previous iteration, that you can use to compare against the records collected on this iteration.

For example, if a service returns a list of all records, and each record looks like so:

{
  "recordName" : "A",
  "createdAt" : "2017-02-15 8:58:00"
}

then, so long as I can keep track of the time the last iteration occurred, I can filter out my list of records to only include those whose "createdAt" value is greater than that timestamp. The "since" value would keep track of that timestamp to filter by, and then would be reset to the current time after the list was filtered.

The "since" value can be defined in the Control.Since module, and once defined may be referred to with Mustache as "{{since}}" throughout that Event method.

####Building a Polling Event Polling Events have a fairly well-defined structure, that can be used in most Connectors. Generally speaking, they'll always include these steps, in approximately this order:

  • Get a list of items from an API
  • Filter that list based off of the since value that was set on a previous execution. As a side note, sometimes services will allow you to provide a timestamp as a query parameter in the initial request. In those cases, this step is unnecessary, and instead you would pass the since value as that query parameter in the initial API interaction.
  • Update your "since" value with the Control.Since module
  • (Optional, but often needed) Map over each item in the filtered list to render that item into the right format
  • End the method with the new list of formatted and filtered items.

Note: A Polling Event must return a list, even if that list contains just one item in a given iteration

####Testing a Polling Event To test a Polling Event in the connector builder, you can artificially set the "since" value in the "Run" window, inside the "Since" textarea. Inside of this area, put in any value that you want to simulate as the since value from the previous iteration of a method execution. You can also leave it as "null" to simulate the first run of your Polling Event.

###Webhook Events

Webhook Events will only run when the corresponding webhook is activated. When the user turns on a FLO that starts with a Webhook Event, Azuqua will call an API endpoint to set up a webhook. Whenever the webhook is activated, the Event will run and trigger the rest of the FLO. When the FLO is turned off, the Event will tear down the webhook.

Webhook Events execute the FLO on-demand, only triggering FLOs when the desired Event occurs. To declare an Event as a webhook Event, click on the ellipses by the Event name, select "Configure Method," and check "This is a webhook."

A Webhook Event has 5 sections:

  • Params, where you declare any parameters that will appear on the card
  • Output, where you declare the output schema of your Event
  • Start, where you declare the steps that will setup the webhook when the user turns on their FLO
  • Core, where you declare the steps to handle incoming data from the webhook
  • Stop, where you declare the steps that will tear down the webhook when the user turns off their FLO

The Start, Stop, and Core sections are all built using modules, the universal building-blocks that you can use to write programs for the Azuqua engine.

##Actions

Actions have 4 sections:

  • Params, where you declare any parameters that will appear on the card. Parameters do not accept any data from previous cards
  • Input, where you declare any inputs that will appear on the card. Inputs can accept data that is dragged and dropped from an earlier card.
  • Output, where you declare any outputs that will appear on the card
  • Core, where you declare the steps (called Modules) that will make the call to the API to perform the Action and handle any data that the API returns.

In general, Actions take in data, use it to run a call to the API, then shape the resulting object into the declared output format.

Note: Actions do not return collections, just single objects. If the last module in your Action outputs a collections object (where "_array" = "true"), you will get a typecasting error.

##Params ###About Parameters You may find that you need information before you can process the inputs a user specifies as you develop your Connector. In these cases, we use params, which are known as "options" in the Azuqua Designer.

A common case where this may happen is when your Connector must render dynamic inputs, that depend on "options" a user must select. A specific example in an Azuqua-made Connector is the "Create Row" method inside the Google Sheets Connector. In this method, we dynamically create the inputs for the user when they create the card, which will show the fields for a row in their preferred Google Sheet. However, before we can render these, we must know the spreadsheet and worksheet that this row will be created in. In this case, the spreadsheet name and worksheet name would be the parameters to this method.

There are two main differences between inputs and params, the first is that values cannot be dragged into a param, they must be entered/selected by the user. The other is that parameters can only be two types, as opposed to an input being one of any of Azuqua's standard types.

There are two types of parameters, which affect how the parameter appears in the Azuqua Designer.

  • String type parameters
  • Option type parameters

Parameters will generally take this form:

Template:

[ 
  { 
    "[paramName]": { 
      "type": "", 
      "displayname": "" 
    } 
  } 
] 

Fields:

  • "paramName": The name of your parameter internally. This name will not be surfaced to the user, but can be used by you to refer to this value in modules inside of this Action/Event method. You may have as many of these as necessary on your Connector. Note, you may not name a parameter "module"
    • "type": the type of your parameter (choices: 'option,' 'string'). Read more about each kind below
    • "displayname": the name that will display above the parameter on the card

You may refer to your parameters with Mustache inside the main Core FLO in this form: "{{params.paramName}}". If you are trying to refer to a parameter inside of a lookup metadata method, see Dependent Dynamic Dropdowns.

###String Parameters String parameters appear as a field the user may type values into. Generally speaking, when tempted to use a string type parameter, you can likely change that field to be an input instead of a parameter. However, there are some string parameters on Events. One example is the Twitter "Monitor Keyword" event, which requires users to type in the keyword they would like to monitor as a parameter.

String parameters are simple to make, and will be very similar to creating the inputs or outputs for a card. So, a string parameter would be defined like this:

Template:

[
   {
      "parameterName": {
         "type": "string",
         "displayname": "Display Parameter Name"
       }
   }
]

###Basic Option Parameters (Dropdowns) Option parameters appear as a dropdown that the user must choose an option from. This is the most commonly used type of parameter. Option parameters can also be set to be:

  • Dynamic- This affects whether or not a Connector parameter is generated at runtime (often based off of a previous parameter, or account information). If an option parameter is not dynamic, then the Connector developer must statically define the dropdown list in the Connector JSON.
  • Dependent- This affects whether or not a Connector parameter will wait for the results of a previous parameter to generate the list of items in its dropdown.

At its most basic, an option parameter will be independent and static. This means that it does not depend on another parameter, and that the contents of that dropdown list will be coded directly into the Connector JSON during the development process. A basic static dropdown parameter looks like this:

"paramNameHere": { 
    "type": "option", 
    "displayname": "Param Name Here", 
    "choices": [ 
        "A", 
        "B", 
        "C" 
    ] 
}

As you'll notice, this looks a lot like the string parameter, except that "type" is now "option" and there is now an array called "choices" that contains a list of strings, each representing an item in the rendered dropdown menu.

###Dynamic Dropdowns

However, static lists are not always sufficient to produce a useful Connector. This brings us to the "dynamic" aspect of a parameter. To dynamically create parameter dropdown lists, we will need to do two things:

  1. Replace the "choices" array, with a "lookup" object, that contains information about how the UI will render the generated dropdown, and how it will interpret the data it grabs. See below for more details.
  1. Set up a helper function (also known as a metadata method) that will generate a list of items to fill your dynamic dropdown menu. The output of this metadata method needs to be a list of items, which we will describe in more detail below.

####The Lookup Object

The lookup object inside of a param informs our engine how to generate the items in a dropdown list parameter. Here's an example of a parameter with a lookup object:

[
   {
      "field1": {
         "type": "option",
         "displayname": "Field 1,
         "lookup": {
            "channel": "connectorname",
            "operation": "getListOfOptions",
            "key": "key",
            "value": "value"
         }
      }
   }
]

The lookup object is composed of these items:

  • "channel" : the name of your connector (not display name)
  • "operation" : the name of the method that will be used to generate the list of options. NOTE: this will be the "compiled" name of the method you are calling. This means that, under the hood, Forge will create a normalized form of every method name in your Connector, that has all non-alphabetic character removed, lower-cased, and camel-cased. For example, if I have "Get List Of Options" as the name of my method, the name I will need to put in this field would be "getListOfOptions". In general, it's best to just name the supporting metadata method in this style as well, to reduce confusion
  • "key" represents the path in your returned list of options (from the supporting metadata method) that our UI will look to generate the "key" of each item in the dropdown list. NOTE: If you're returning a simple list of strings, then you may omit this field
  • "value" represents the path in your returned list of options (from the supporting metadata method) that our UI will look to generate the "value" of each item in the dropdown list. NOTE: If you're returning a simple list of strings, then you may omit this field

####Helper methods (Metadata Methods)

All metadata methods that are generating a param must result in a list as the final output. At its most basic, that list may be a simple list of strings, like so:

["Item A", "Item B", "Item C"]

However, more commonly, you will need a variety of items to come back. This type of list should look like this:

[
	{
		"name" : "Field 1",
		"type" : "string"
	},
	{
		"name" : "Field 2",
		"type" : "number"
	}
]

However, you may also return a list of items with more information. So long as the list that's being returned from the metadata method contains, a "type", and a field that will be used as the "key" (and "value", if that is different than the "key" value) in your dropdown list. That could look like so:

Lookup Object

"lookup": {
   "channel": "connectorname",
   "operation": "getListOfOptions",
   "key": "name",
   "value": "id"
}

Returned List

[
  {
    "id" : 123,
    "name" : "Item A",
    "randomField" : 1
  },
  {
   "id" : 345,
   "name" : "Item B",
   "randomField" : 3
  }
]

Rendered Dropdown in Azuqua Designer

["Item A", "Item B"]

###Dependent Dynamic Dropdowns Dependent dropdown params are parameters that cannot be rendered until a previous parameter has been selected. A dependent dropdown is always dynamic.

Implementing a dependent dropdown param is very similar to implementing a dynamic dropdown. The only difference is that a dependent parameter also has a "dependsOn" field. Here is a sample set of params, where the second field must be selected after the first field has been chosen.

[
 {
   "field1": {
     "type": "option",
     "displayname": "Field 1,
     "lookup": {
       "channel": "connectorname",
       "operation": "getListOfOptions",
       "key": "key",
       "value": "value"
      }
    },
    "field2": {
       "type": "option",
       "dependsOn": [
         "field1"
        ],
       "displayname": "Field 2",
       "lookup": {
         "channel": "connectorname",
         "operation": "getListOfOtherOptions",
         "key": "key",
         "value": "value"
       }
     }
   }
 ]

When a dependent dropdown list is generated, you will need the value of the parameter that your field depends on to generate the new field. For example, if my first parameter is "country" and my second is "state/province", then I will need whatever value the user chooses in that first parameter to then generate the list of options for that second parameter.

To do this, I can refer to any selected parameter value as "{{input.paramName}}" inside the metadata method used to generate my dependent dropdown list. For example, inside of the metadata method for that "state/province" option list, I will need the first value the user chooses. So, in this example, in my metadata method called "getStatesForCountry", I would reference the "country" option by using "{{input.country}}"

##Inputs ###About Inputs Inputs are the data that your Action (not Events) will use to complete its designed task. The Input section of an Action defines the input fields on your Connector and provides a schema to build the object that will be passed to the Core code to process.

The object defined by the input section must follow this structure:

{ 
  "extensible": false, 
  "attributes": [ 
    { 
      "name": "[HeaderName1]", 
      "attributes": [ 
        { 
          "name": "[FieldName1]", 
          "type": "string" 
        } 
      ] 
    } 
  ] 
} 

There must be at least one "attributes" array, that contains all of the objects that represent "Headers" on your Connector card. A "Header" is a category of input fields, used to organize the variety of inputs your Connector may have. Inside of a "Header" object, there is an array, which will contain your "Field" objects. "Field" objects are where actual data is passed into your Action.

An example of a "Header" would be "User", which contains a 'Field" called "Username".

This input schema would generate an input object that looks like this, after the user has entered data:

{ 
    "[HeaderName1]": { 
         "[FieldName1]" : [value]  
     } 
} 

To access this field in the rest of your method, you could use the Mustache template {{input.[HeaderName1].[FieldName1]}}.

###Static Inputs Most input sections are static--the user will always see the same input fields on this card, regardless of their account data. For example, the "Post Tweet" Action from Twitter will always have the same input fields.

Template:

{ 
    "extensible" : false 
    "attributes" : [ 
        { 
            "name": "", 
            "attributes": [ 
                { 
                    "name": "", 
                    "type": "string" 
                } 
            ] 
        } 
    ] 
} 

Fields:

  • "extensible": A boolean that if 'true' will allow users to add their own custom input fields. This should only be used if you are unable to dynamically generate users' custom fields using a metadata method
  • "attributes": An array that contains the header objects
    • "name": The name of the header object. This will be displayed on the card
      • "attributes": An array that contains the input fields
        • "name": the name of the input field. This will be displayed on the card.
        • "type": must be 'string'.

Example:

{ 
    "extensible": false,  
    "attributes": [ 
        {  
            "name": "User", 
            "attributes": [ 
                { 
                    "name": "firstName", 
                    "type": "string" 
                }, 
                { 
                    "name": "lastName", 
                    "type": "string" 
                }, 
                { 
                    "name": "email", 
                    "type": "string" 
                } 
            ] 
        },  
        { 
            "name": "Company", 
            "attributes": [ 
                { 
                    "name": "name", 
                    "type": "string" 
                },  
                { 
                        "name": "revenue", 
                        "type": "string" 
                } 
            ] 
        } 
    ] 
} 

###Dynamic Inputs You can also take the user's account information and use it to dynamically generate inputs. This should only be done if the application supports custom fields for the record type you are dealing with. You can use a Metadata method to call the API and fetch the users' custom fields, and then build them into the card.

Template:

{ 
    "extensible": false, 
    "attributes": [ 
        { 
            "name": "Normal Fields", 
            "attributes": [ 
                { 
                    "name": "Field1", 
                    "type": "string" 
                }, 
                { 
                    "name": "Field2", 
                    "type": "string" 
                } 
            ] 
        }, 
        { 
            "name": "Custom Fields", 
            "attributes": [ 
                { 
                "metadata": "", 
                "config": "", 
                "data": {} 
                } 
            ] 
        } 
    ] 
} 

Fields:

  • "extensible": A boolean that if 'true' will allow users to add their own custom input fields. This should only be used if you are unable to dynamically generate users' custom fields using a metadata method
  • "attributes": An array that contains the header objects
    • "name": The name of the header object. This will be displayed on the card
      • "attributes": An array that contains the input fields
        • "name": the name of the input field. This will be displayed on the card.
        • "type": must be 'string'.
        • "metadata": The name of the metadata method you would like to call

The metadata method should call the API to obtain data about the user's custom fields, then return an array of strings. Each string in that array will be turned into a field.

Example:

{ 
    "extensible": false, 
    "attributes": [ 
        { 
            "name": "Ticket", 
            "attributes": [ 
            { 
                "name": "Name", 
                "type": "string" 
            }, 
            { 
                "name": "Description", 
                "type": "string" 
            } 
        ] 
    }, 
    { 
        "name": "Custom Fields", 
            "attributes": [ 
                { 
                    "metadata": "getTicketFields", 
                    "config": "", 
                    "data": {} 
                } 
            ] 
        } 
    ] 
} 

The above example would call a metadata method called getTicketFields. getTicketFields would then run a call to the API, and render the results into an array.

Then, in the Core Code, you can use modules to manipulate the input data before passing the data to the API.

##Output ###About Outputs The Output section defines the output object that the Connector card expects from your method. This information is used to generate the card at design time, so the user can drag their data from one card to another without reference to the API underlying the card. If the result of the module steps is not the same as the object declared in the output schema, the data will not be passed to any following cards.

The object defined by the output section must follow this structure:

{ 
  "extensible": false, 
  "attributes": [ 
    { 
      "name": "[HeaderName1]", 
      "attributes": [ 
        { 
          "name": "[FieldName1]", 
          "type": "string" 
        } ,
        {
        	"name": "[CollectionFieldName1]",
        	"type": "string",
        	"collection": true
        },
        {
        	"name": "[ObjectFieldName1]"
        	"type" : "object",
        	"collection" : false,
        	//required for all outputs of type "object"
        	"attributes": [
        		"fieldOne" : value
        	]
        }
      ] 
    } 
  ] 
} 

There must be at least one "attributes" array, that contains all of the objects that represent "Headers" on your Connector card. A "Header" is a category of input fields, used to organize the variety of inputs your Connector may have. Inside of a "Header" object, there is an array, which will contain your "Field" objects. "Field" objects are where actual data passed into your Action.

An example of a "Header" would be "User", which contains a 'Field" called "Username".

This input schema would generate an input object that looks like this, after the user has entered data:

{ 
    "[HeaderName1]": { 
         "[FieldName1]" : [value]  
     } 
} 

###Static Outputs Most output objects are static--the user will see the same output fields on the card, regardless of their account data. For example, no matter the user, the "New Card in List" Event from Trello will always have the same output fields.

Template:

{ 
    "extensible" : false 
    "attributes" : [ 
        { 
            "name": "", 
            "attributes": [ 
                { 
                "name": "", 
                "type": "string" 
                } 
            ] 
        } 
    ] 
} 

Fields:

  • "extensible": A boolean that if 'true' will allow users to add their own custom output fields. This should only be used if you are unable to dynamically generate users' custom fields using a metadata method
  • "attributes": An array that contains the header objects
    • "name": The name of the header object. This will be displayed on the card
    • "attributes": An array that contains the output objects
      • "name": The name of the output field. This will be displayed on the card
      • "type": must be 'string'

Example:

{ 
    "extensible": false,  
    "attributes": [ 
        {  
            "name": "Post", 
            "attributes": [ 
                { 
                "name": "Text", 
                "type": "string" 
                },  
                { 
                "name": "URL", 
                "type": "string" 
                },  
                { 
                "name": "Reblogs", 
                "type": "string" 
                } 
            ] 
        },  
        { 
            "name": "Author", 
            "attributes": [ 
                { 
                    "name": "Username", 
                    "type": "string" 
                },  
                { 
                    "name": "Followers", 
                    "type": "string"            
                } 
            ] 
        } 
    ] 
} 

This output schema would expect an output object from the modules section that looks like this:

{ 
	"Post": {  
		"Text": "Bagels are just donuts without the happiness.",  
		"URL": "www.donutfans.com/post/1029384756", 
		"Reblogs": "47" 
	}, 
	"Author": { 
		"Username": "KrullerKing", 
		"Followers": "189" 
	} 
} 

###Dynamic Outputs You can also take the user's account information and use it to dynamically generate outputs. This should only be done if the application supports custom fields for the record type you are dealing with. You can use a Metadata method to call the API and fetch the users' custom fields, and then build them into the card.

Template:

{ 
    "extensible": false, 
    "attributes": [ 
        { 
            "name": "Normal Fields", 
            "attributes": [ 
                { 
                    "name": "Field1", 
                    "type": "string" 
                }, 
                { 
                    "name": "Field2", 
                    "type": "string" 
                } 
            ] 
        }, 
        { 
            "name": "Custom Fields", 
            "attributes": [ 
                { 
                    "metadata": "", 
                    "config": "", 
                    "data": {} 
                } 
            ] 
        } 
    ] 
} 

Fields:

  • "extensible": A boolean that if 'true' will allow users to add their own custom input fields. This should only be used if you are unable to dynamically generate users' custom fields using a metadata method
  • "attributes": An array that contains the header objects
    • "name": The name of the header object. This will be displayed on the card
    • "attributes": An array that contains the input fields
      • "name": the name of the input field. This will be displayed on the card.
      • "type": must be 'string'.
  • "metadata": The name of the metadata method you would like to call The metadata method should call the API to obtain data about the user's custom fields, then return an array of strings. Each string in that array will be turned into a field.

Example:

{ 
    "extensible": false, 
    "attributes": [ 
        { 
            "name": "User", 
            "attributes": [ 
                { 
                    "name": "Full Name", 
                    "type": "string" 
                }, 
                { 
                    "name": "Email", 
                    "type": "string" 
                }, 
                { 
                    "name": "ID", 
                    "type": "string" 
                } 
            ] 
        }, 
        { 
            "name": "Custom Fields", 
            "attributes": [ 
                { 
                    "metadata": "getTicketFields", 
                    "config": "", 
                    "data": {} 
                } 
            ] 
        } 
    ] 
} 

The above example would call a metadata method called getTicketFields. getTicketFields would then run a call to the API, and render the results into an Array. The result of the Core array must be the same shape as the output object defined in your schema. In the example above, if the user had two custom ticket fields, "plan_type" and "csm_owner", the output object produced by the Core modules should look like this:

{ 
    "User": { 
        "Full Name": "Pseudo Ersatzson", 
        "Email": "pseudo@illusory.com", 
        "ID": "0112358132134" 
    }, 
    "Custom Fields": { 
        "plan_type": "Professional", 
        "csm_owner": "John Smith" 
    } 
} 

#Using Modules ##About Modules

You've set up your Authentication and the framework of an Event/Action, but now you actually need to connect your Connector to an API. You can do this using Azuqua's pre-built library of declarative building blocks, known as modules. Basically, modules lay out a set of steps that your Connector should execute at run-time, determining how user data is turned into API requests, and how API data is in turn rendered into a user-friendly format.

In the "Core" section of your method (or, in the case of webhooks, the "Start" and "Stop" sections) you can declare a chain of modules that will handle everything from HTTP requests to JSON parsing and data manipulation. Modules perform in the order they appear in a method.

All modules will generate a template when selected. That template takes this general form:

{ 
  "brick": "module_class.module_name", 
  "id": "id_name", 
  "inputs": { 
  	  "input1": {
  	  	   "_availableTypes" : [
  		  	type1, 
  		  	type2
  		  ],
  		  "_type": "type1", 
  		  "_array": true/false, 
  		  "_value": "" 
	  }, 
      "input2": { 
  		  "_availableTypes" : [
  		  	type1, 
  		  	type2
  		  ],
  		  "_type": "type1", 
  		  "_array": true/false, 
  		  "_value": ""  
	   } 
  }, 
  "outputs": { 
      "output1" : { 
		  "_type": "", 
  		  "_array": true/false
	   } 
   } 
}

Fields:

  • "brick": This is the internal name for modules, and represents the class and function that you are choosing to use in the given module. This will be populated after selecting a module of your choice.
  • "id": Is an optional parameter. It can either remain the calculated ID, or can be set to any value unique to the method the module exists within. When set, it can be used by other modules to refer to the output of that module.To reference this specific module in other modules, you can use the Mustache template {{[idName].[any of its output keys]}}
  • "inputs": Represents the set data that will used inside of the module to complete its given process. For more information, see "Module Inputs".
  • "outputs": Represents the set of data that has been processed by the module's function and is available to be used in other modules within the same method or accompanying Metadata method. For more information about outputs, see "Module Outputs".

##Inputs and Outputs ###Module Inputs "Inputs" represents the structure of data being passed into the module, and how those values will be referenced throughout the module. It may contain any number of inputs, but modules will only read a certain set of predefined inputs, as denoted in the individual module documentation. An "inputs" object will always load a template in this form:

"inputs": { 
	"input1": { 
		"_availableTypes" : [
			type1, 
			type2
		],
		"_type": "type1", 
		"_array": true/false, 
		"_value": "" 
	}, 
	"input2" : { 
		"_availableTypes" : [
			type1, 
			type2
		],
		"_type": "type1", 
		"_array": true/false, 
		"_value": ""
	} 
} 

In this format, the values of each key besides "_value" will auto-populate with the default values for the given module. The keys that are available are:

  • "_availableTypes": The list of types this input object is allowed to be with the given module. You may change "_type" to any of the values given in this array. Changing this array will not affect your Connector in any way. If this field is absent, that means that the only type this input could be is the default type.
  • "_type": The type of the object that will be inputted to this module's underlying function. This will auto-populate to the default type for that input. You may change the "_type" value to match any of the types listed in the "_availableTypes" array, depending on what type of object you would like input into the module.
  • "_array": Whether or not this input is a collection. When marked true, this object will become a collection of the already denoted type from the "_type" key.
  • "_value": The value that the input object will have. You will need to change "_value" to set the value for that input. This is the field you will be using the most throughout your Connector development experience.
  • (Optional) "_defaultValue": The default value of the input object. May be set to any value as long as its type is consistent with that input's "_type" value.

Depending on your input object's "_type" value, you may set its value in many different ways.

If the input's "_type" value IS NOT "flo", you may set the input object's value to any of the following forms, permitting the value you use is the same type as your input's "_type" value:

  • A Mustache reference, i.e. {{objectName}} or {{moduleID.[outputFieldHere]}}. In this case, the input value is the exact JSON of objectName, and is not stringified or escaped. See Passing Data With Mustache, for more information about what these references can look like.
  • A string containing a reference, i.e. "Bearer {{auth.access_token}}". In this case, the input type is a string and the value of auth.access_token should be a string (or it will be converted). Under the hood, this performs a string concatenation.
  • A string without Mustache, i.e. "application/json". In this case, the input type is string.
  • An object. Any object is simply treated as an input of type "object". The object can contain keys with values that have any valid JSON. Both keys and values are parsed for Mustache references.
  • An array. The array can contain any valid JSON as its values. Mustache references can be included in any value of the array.
  • A number. Parsed literally.
  • A boolean. Parsed literally.
  • null. Parsed literally.

If the input's "_type" IS "flo", you may set "value" to any of the following forms:

  • A single module, contained in the "_value" object. You may omit the "_availableTypes" and "_defaultValue" keys for that module's inputs. This will look like so:
"flo": { 
	"_type": "flo", 
	"_value": { 
		"brick": "object.construct", 
		"inputs": { 
			"input1" : {
				"_type" : "object",
				"_array" : true,
				"_value" : "{{reference_here}}"
			} 
		}, 
		"outputs": { 
			"output": {
				"_type" : "object", 
				"_array" : "false"
			}
		} 
	} 
} 
  • An array of modules, contained in the "_value" object.
  • A string that refers to the ID of a method, e.g. "methodNameHere".

Notes:

  • When accessing paths of a key like "prevData" or "input", which both represent objects passed into the current module from a previous module, an object.get is performed on the referenced key unless the output can be found on that exact module. For example, if prevData is an http.get method, it will expose body, statusCode, and headers as outputs. Thus, you can reference {{prevData.body}} and this will be able to grab the exact reference. Using {{prevData.body.ok}} will produce an object.get on prevData.body.
  • Using Mustache with object keys results in another method being used to set the key generated by Mustache. Generally speaking you'll produce faster FLOs if you know the keys of your hardcoded object in advance.

###Module Outputs

"Outputs" represents the values that are produced after running a given module. All keys inside of the outputs schema are available in any other module in the same method by using the Mustache template {{moduleID.[outputName]}}.

Outputs take a similar form as inputs, with a few changes. The values in the "outputs" object are now meant to act as a reference so that you can know the shape of what comes out of your module. Now, there are no "_value" or "_defaultValue" keys, although you may add these fields and change them, if you would like.

Outputs look like this:

"outputs": { 
	"output1": { 
		"_type": "", 
		"_array": true/false 
	}, 
	"output2" : { 
		"_type": "", 
		"_array": true/false 
	}
}

The remaining fields are:

  • "_type": The default type of the output object. You often will not need to change this, however, there are a few important cases where you would need to change the "_type" value.
    • For example: The "Object.get" module and the "JSON.Scope" module will both output a value from an object, but the "_type" value will default to "object", in which case you may want to change that output's "_type" to match the type you are expecting from the object you are pulling from.
  • "_array": Whether or not the output object is a collection.
    • If you are developing an Action method, your last module cannot output a collection, because all Actions expect an "object" type. In this case, follow up the module that outputs a collection with a JSON.Render brick or an Object.Construct to create the object you need.

Note: The type and format of an output object is crucial to Connector development, because the expected format of the "Output" section of a method must match whatever the output of the last module in that method is. If your Event/Action method is expecting an object with a string field called "X", your last module must output an object with a string field called "X".

##Passing Data With Mustache ###Using Mustache Azuqua uses Mustache templates to hash data, so that it may be passed around modules within a method. Using Mustache, you can reference the parameters and inputs of your Event/Action method from any module within that method. Additionally, you may pass any data from the output schema of any module into any other module in the same Event/Action method.

For example, if you're building an Action card that creates a new resource (e.g. building a new task), you'll probably need to take information that the user gives you and then send it to the API.

When you set up your inputs you may have included a field for the user to put in the name of their task:

{ 
    "name": "Task", 
    "attributes": [ 
        { 
        "name" : "Task Name", 
        "type": "string" 
        } 
    ] 
} 

The resulting input object will look something like this:

   { 
        "Task: { 
            "Task Name": "" 
        } 
    } 

Later, you'll want to hash this data into the body of your API request. This is where Mustache comes in handy:

"body": {  
    "task_name": "{{input.Task.Task Name}}" 
} 

At runtime, the Mustache will be swapped out for the data that the user enters or drags over from another card.

###Universal Mustache Tags Inside of Azuqua, there are several Mustache tags that can be used anywhere within a method. These will come in handy as you develop your Connectors:

  • {{moduleID.[fieldName]}}, This is how most data will be passed around. Modules and their data can be referenced directly by any other module inside of the Action/Event method this way. Additionally, Metadata methods referenced by the module (with moduleID) can access that module's output data using this Mustache reference. This is done by using the desired module's ID and attaching the desired output field via a path.
  • {{params.[parameterName]}}, where "parameterName" is the name of your parameter object in the Parameters section of your method. Use this tag to refer to the value of any parameters that the user enters.
  • {{input.HeaderName.FieldName}}, where HeaderName is the "name" value in a header object in your "input" section, and FieldName is the "name" value of a field you would like to reference within that header object.
    • Use this tag to refer to any data that the users enters as an input.
    • You can also use it to reference inputs inside of Metadata methods that have been explicitly declared from the calling module or schema.
  • {{prevData.[outputField]}. The object "prevData" represents the module directly preceding the module that this reference is used inside of. For example, if you use {{prevData.outputFieldName}} in the second module inside of a method's Core section, it will grab the outputFieldName value from the first module in that method's Core section.

Note: All Mustache references are case-sensitive!

###Conditional Templates There are lots of cases where you may want to only include mustache templates if certain conditions are met. The most common is when you are updating a resource. If the user doesn't enter data for a certain field, it will be passed to the modules as an empty string. However, you don't want to overwrite data by sending an empty string to the API.

For example, if we're updating a contact, the input section of the card might look something like this:

{ 
     "name": "Contact", 
      "attributes": [ 
        { 
            "name": "Contact Name", 
            "type": "string" 
        }, 
        { 
            "name": "Contact Email", 
            "type": "string" 
        } 
    ] 
} 

...and the body of the API request looks like this:

"body": { 
    "contact_name": "{{input.Contact.Contact Name}}", 
    "contact_email": "{{input.Contact.Contact Email}}" 
} 

However, the user may not want to update all of these fields when they actually run the card. When the FLO actually runs, the resulting input object built by the card might look like this:

{ 
    "Contact": { 
        "Contact Name": "" 
        "Contact Email": "demo@azuqua.com" 
    } 
} 

Then, if the empty data is mustached into the body of the call to the API, the API would be sent an empty string:

"body": { 
    "contact_name": "", 
    "contact_email": "demo@azuqua.com" 
} 

This would update the email like the user wants, but overwrite the existing contact_name with an empty string. Instead, you can use Mustache's built-in logic to check if the fields exist before templating them into the API request.

To help with this, Mustache can evaluate an expression with the # character. If the expression evaluates to true, then Mustache will include the value between the Mustache references (one opening with the # character, the other closing with the / character). You may also put a Mustache reference in between the two conditional templates. You can use the ^ character in place of the # character to run a function that will only include the value between the brackets if the expression evaluates to false.

Example:

"body": { 
    "{{#input.Contact.Contact Name}}contact_name{{/input.Contact.Contact Name}}": "{{input.Contact.Contact Name}}", 
    "{{#input.Contact.Contact Email}}contact_email{{/input.Contact.Contact Email}}": "{{input.Contact.Contact Email}}" 
} 

This time, when the user doesn't enter a value for a field, Mustache will check to see if the field exists before including it in the body of the request to the API. Now, the request to the API will look something like this:

"body": { 
    "": "", 
    "contact_email": "demo.azuqua.com" 
} 

The contact_name field isn't included, since the user didn't enter a value. That way, the email will be updated, and the existing contact name will be left alone.

#Metadata Methods ##Using Metadata Methods Metadata methods are helper methods that supplement the Events and Actions in your Connector. Some uses of Metadata methods include:

  • Generating the arrays that populate dynamic dropdowns
  • Building dynamic input and output schemas that include users' custom fields
  • Containing code that will be re-used in several methods
  • Executing branches from main methods
  • Executing complex loops
  • Error handling

Metadata methods only have one section, the Core array where you declare the set of modules that execute the method. Since they do not appear in the UI, they do not require parameters, input, and output schemas.

Metadata methods are helpful because they can unload work off of other modules, meaning that data passing from the Event/Action method into the metadata method is crucial. At its most fundamental level, any field that is an output of the Event/Action method that is calling the metadata method can be called with the Mustache reference {{input.field_name}}. For more specific cases, such as metadata method classes and context passing, read on.

##Metadata Method Classes ###General Metadata methods can serve many different use cases, but sometimes special classes must be added to a metadata to allow certain kinds of functionality to run smoothly. The three types of metadata methods that require an additional metadata method class are iterative metadata methods, list.reduce, and all error handling metadata methods. These classes must be added at the top level of a metadata method object, before any inputs/outputs it accepts. Read on for more information about each type.

###Iterations Many commonly used modules that are collection-based require an input that acts as its own FLO (FLO input), which handles functionality that operates on each element. Referencing the Module Library documentation for each module should indicate which modules will require a FLO input or not. For these FLO inputs, you must specify "_type" as "flo". You then specify what this FLO will do by providing a reference to a metadata method's name that will act as the FLO you want operating on each element.

Example:

{ 
  "brick": "list.map", 
  "input": { 
    "collection": "{{prevData.array}}", 
    "concurrency": 4, 
    "flo": { 
      "_type": "flo", 
      "_value": "[metadataMethodID]" 
    } 
  } 
} 

After marking the FLO input in your Event/Action method, you then need to create a flag in the accompanying metadata method to indicate its use as an iteration method, which has consequences for how inputs are mapped into the function. Inside the method declaration (when you click the name of your metadata method on the left menu), the "variant" flag needs to be inserted at the top level of the metadata object.

Example:

{ 
  "name": "[metadataMethodName]", 
  "kind": "metadata", 
  "variant": { 
    "_type": "object", 
    "_key": "[itemName]", 
    "_array": false
  }, 
  "zebricks": [ 
    { 
     //modules you create will go here 
    } 
  ] 
} 

If you want to reference the item of the current iteration, you use {{itemName.path_goes_here}}. When declaring the key to reference the current iteration, you can also choose to declare what the type of the item is (optional "_type" key), whether it's an array or not (optional "_array" key), and the defaultValue it should be if undefined or null (optional "_defaultValue" key).

###List.reduce (memo) For methods like List.reduce, which use a memo property, you declare a memo property at the same namespace as the variant property. This memo property can be used to keep track of state, as the list is being reduced. It defaults to an empty object, but you may change this to be any other value. Memo may also be used with other class objects, for example:

{ 
  "name": "metadata_method", 
  "variant": { 
    "_type": "object", 
    "_key": "item", 
    "_array": false
  }, 
  "memo": { 
    "_type": "object", 
    "_array": false
  }, 
  "zebricks": [ 
    { 
      // modules you create will be placed into this array 
    } 
  ] 
} 

From here, you can reference this memo property as {{memo.path_to_here}}.

###Error Handling Cases arise where we need try/catch logic. This is accomplished with error handling methods. In the method we want to catch errors in, we use the handler key in the following way:

{ 
  "brick": "json.parse", 
  "id": "jsonParseCatch", 
  "inputs": { 
    "string": "{{otherExec.output}}" 
  }, 
  "handler": { 
    "method": // method name goes here, 
    "merge": "join"/"overwrite"/"throw", 
    "path": // only used in overwrite, must be a valid method output key 
  } 
} 

Fields:

  • "method": Determines which method should process the error
  • "merge": represents how the output of the error is handled
    • If merge is set to "join": The output of the error handler is joined with the output of the method; only outputs that match the outputs of the method using the handler will be used, i.e. output for json.parse. This is the default behavior.
    • If merge is set to "overwrite": The output of the error is written under the string specified by the path property onto the method's output. This key must be a valid output for the method, i.e. output for json.parse.
    • If merge is set to "throw": The output of the error is thrown as an error, breaking execution up until another piece of handler logic catches it.

For the Metadata method being used as an error handler, an error key is used similarly to the variant key for metadata methods used as iterators. For example:

{ 
  "name": "metadata_method", 
  "error": { 
    "_type": "object", 
    "_array": false, 
    "_defaultValue": {} 
  }, 
  "zebricks": [ 
    { 
      // modules you create will go here 
    } 
  ] 
} 

Then to access properties of the error object, reference it with Mustache as {{error.path_to_here}}.

##Passing Context In some instances, you'll want to carry over context from an action or one metadata method into another metadata method, but not want to pass it into the inputs (i.e. a metadata method used for collections operations). To do this, you pass this data under the context group. To utilize the context group, you must add a key to your Event/Action method to define what kind of information to pass over. For example, if you had a previous method with the id "some_method", and you wanted access to all of the output of that method in a spawned metadata method used for collections, you'd need to write something like this:

{ 
  "brick": "array.map", 
  "input": { 
    "collection": "{{prevData.array}}", 
    "concurrency": 4, 
    "context": { 
      "ref_to_use": { 
        "_type": "object", 
        "_array": false, 
        "_value": "{{some_method.output}}" 
      } 
    }, 
    "flo": { 
      "_type": "flo", 
      "_value": "metadata_method" 
    } 
  } 
} 

Then, in order to reference "ref_to_use" in that metadata method, you would need to declare a context group for that metadata method, similar to the memo and variant keys. In the below example, you would need to add both a "context" group and "variant" key, because you are both passing in information ("context") and iterating through a collection ("variant").

{ 
  "name": "metadata_method", 
  "variant": { 
    "_type": "object", 
    "_key": "item", 
    "_array": false
  }, 
  "context": { 
    "ref_to_use": { 
      "_type": "object", 
      "_array": false
    } 
  }, 
  "zebricks": [ 
    { 
      // modules go here 
    } 
  ] 
} 

Inside your modules for that method, you can then reference "ref_to_use" (or whatever you name your reference) as {{ref_to_use.path_to_here}} with Mustache.

#Testing ##General Testing Now that you've set up some Events and Actions, the next step is to test your Connector's capabilities. Forge, our Connector development platform, allows you to perform multiple layers of testing as you develop.

In general, testing while developing your Connector happens in two ways:

  • Testing a single method
  • Testing your entire Connector

The first way, testing a singular method, allows you to test the flow of data through your method, in a low-stakes, very transparent way. You control the data that is input into your Connector's method explicitly, and can therefore benefit from having total control over the cases your Connector is tested against. This style of testing all occurs within Forge itself, and is designed to be used frequently along your development cycle. For more information, see "Testing Your Methods".

The second way, testing your entire Connector, happens inside of the Azuqua Designer itself, and allows you to thoroughly test all of the functionality and visual elements of your Connector in one place. In designer, you can also test your Connector's interactivity with other Connectors- allowing you to see how "usable" your Connector would feel to an Azuqua user. This style of testing is meant to be robust, but should be used during the "polishing" stage of your testing workflow after many rounds of testing within Forge, as this method requires that you must deploy to the designer environment with each update.

##Testing Your Methods ###How to Run a Test To test your work as you develop each method of your Connector, you should run your tests within Forge. This allows for quick turnaround on adjusting your methods and modules as you go, alongside your code.

To run a test within Forge, select the method you would like to test, and then select the "Run" button in the upper right. Once clicked, the Run panel will pop up. In this panel, there will be a text field for Auth, Params, Inputs and Since.

For each text field, there will be a generated code schema that matches the schema specified in that method. For example, if your "Input" section has a "Character" header with the field "Character ID", this text box will have a "Character" header with a spot to input the value you would like to test with.

To run a test, you should fill in each area of your Auth, Params and Inputs schema with the values that you would like to test against. It is not necessary to fill in values for blank or empty schemas, the test will run without it. However, you cannot run an Event method without inputting a "Since" value, so you can simulate grabbing data properly.

Once everything is set up, push "Submit". At this point, all of the testing data will be generated as logs created by Forge as it simulates a run of that method.

###Interpreting Logs Once you've ran a test, you will see logs appear in the console below your code. On the left side of the console is a log navigation panel, and on the right side will be the actual JSON objects and responses passed around between the modules in your method.

The left navigation panel is sorted by the time that the step is executed, and is labeled by the type of module that enacted function originated from. Once a step on the left side is selected, the right side of the pane will auto-scroll through the list of responses to the response that corresponds to that log. You will be able to see the shape and values of the JSON objects you are manipulating to run your Connector.

Errors will show up in red, and can be filtered to by selecting the "Errors" tab. These runtime errors will show you which step failed, and from what call.

###Using Control.Log

If in the case you want to control your own logs, Forge has a Control.Log module that will allow you to output logs into our logging console that you fully control. To use this, insert a Control.Log module into your method relative to where you would like it to show up in the Forge logger. For example, if you want a log to appear at the end of a method's execution, you should put the Control.Log into your method as the last module.

Then, to specify what content you would like placed in the Forge logger, you will need to place your desired logs as items within the Control.Log's "inputs" object.

Example:

{ 
  "brick": "control.log", 
  "id": "log brick", 
  "inputs": { 
     "[insertName]" : "Test!!!!!!!!!!!!!!!! + {{input.Character.CharacterID}}", 
  "outputs": {} 
} 

##Testing In Designer

There are three stages of Connector versions management- saving, submission and deployment.

The first step is "Saving". Saving will overwrite the existing "DRAFT" version, which exists outside of the Semantic Versioning system our Connectors use. There is only one universal "DRAFT" for all Connector developers in an organization. Currently there is little protection of this "DRAFT" version from being overwritten, so take precautions when sharing a Connector during its development phase.

When you save a Connector, it does not need to compile to successfully be stored for later development. To save, either push the "Save" button in the toolbar or use Command/Control + S.

The next stage is "submission". At this stage, you are saving the "DRAFT" as an actual Semantic Version of your Connector. For more information about this, see "Semantic Versioning".

At this stage, the version is getting saved specifically into our system, so it must compile. You cannot submit a Connector with compilation errors inside of its "Core" sections.

Compilation at this stage and during the saving stage does not check for UI compilation errors. To Submit, push the "Submit" button.

After this, you may choose to deploy your Connector. Deployment means that a Connector version will be exposed in Designer for your organization. To go to the deployment panel, choose the "Control Panel" menu item in the top menu of the Azuqua Connector builder.

Here you will see that there are two types of deployment:

  • "Test" Connectors
  • "Production" Connectors

"Test" Connectors are versions of a Connector that are deployed specifically for testing purposes. Deploying a new version of a "Test Connector" will not impact the "Production Connector" or any of the FLOs that use the "Production" version of your Connector- meaning you can test safely with the knowledge that no FLOs that non-developers are using would be impacted. "Test" Connectors may be removed from Designer, and are hidden by default by users who have not opted into Connector development tools.

"Production" Connectors are versions of a Connector that are intended to be "safe" to build FLOs on top of in your organization. This means that these Connectors are only upgraded to a new version when extensive testing has occurred on that version with a "Test" Connector.

You may have a "Test" version of a Connector deployed and "Production" version of the same Connector deployed at the same time. However, they may not be the same version of the Connector.

The responsibility for maintaining that relationship between "Test" Connectors and "Production" Connectors is on organization administrators. The only checks that occur inside the system to ensure FLO safety during Connector development are:

  • You cannot set a "Test Connector" version to be the existing "Production Connector" version
  • If you set a "Production Connector" version to be the current "Test Connector" version, you will get a warning that it will be replaced.
  • Members may not change the "Production Connector" version

Once you know which type of deployment you want to do and what version you want to deploy, select the right version on the dropdown for your desired Connector, and select the deployment button on the bottom right. Your Connector should be updated in Designer shortly.

#Advanced Topics ##File System ###Multipart Form-Data Upload

NOTE This will be referring to Box's "Upload File" API endpoint. Documented here.

  1. Anatomy of a Multipart Form-Data Request
  2. General Structure
  3. Doing this in Forge
  4. Testing with the FS

####Anatomy of a Multipart Form-Data Request

Before we dissect what a form-data request looks like, here's necessary setup:

  • Run nc -l -p 8000 to create a netcat server listening on localhost:8000. This will display the body of our multipart request. If you don't have nc installed, just look up how to install the GNU netcat utilities onto your system (sorry windows).
  • Create a .txt file called 'stuff.txt' with 'asdf' as the content.
  • Write this to a .js file (I'm using stuff.js because I'm lazy):
var request = require("request"),
fs = require("fs");

request({
  url: "http://localhost:8000/",
  formData: {
    key: "value"
  }
}, function (err, res, body) {
  if (err) console.log("ERROR", err);
  else console.log("BODY", body);
  process.exit();
});

Now that we have our setup complete, let's try running node stuff.js. If you look over at the terminal running nc -l -p 8000, you should see something that looks like this:

POST / HTTP/1.1
host: localhost:8000
content-type: multipart/form-data; boundary=--------------------------180610939475792226718191
content-length: 163
Connection: close

----------------------------180610939475792226718191
Content-Disposition: form-data; name="key"

value
----------------------------180610939475792226718191--

Let's note the following:

  • Lines above Connection: close are headers sent with this HTTP request. The good stuff starts 2 lines after this.
  • ----------------------------180610939475792226718191 is what's referred to as the boundary of this request; it's used to deliminate each key/value pair of the form-data request
  • Underneath that line should be something that looks like the above headers, but the request module in node doesn't send any headers with each piece. We could see 'content-type', 'content-length', or any set of headers.
  • After this follows the disposition parameters, which are used to describe the chunk of data for this key/value pair. For example, we see name="key", which is the key part of our form-data chunk. We could also see data such as 'filename' (which is used when sending local files over the wire).
  • Finally comes the form-data value, where we see "value". If we sent over another form-data key/value pair, we'd see the structure as above but with a different 'name' and value section.
  • To terminate this request, a multipart EOF line is sent (----------------------------180610939475792226718191--).

If you want to see what disposition parameters look like in the request, change formData to:

{
  key: fs.createReadStream("stuff.txt")
}

From there, just rerun nc -l -p 8000 and node stuff.js.

And thus follows the anatomy of a multipart form-data request! Now, let's get to how the FS structures it's multipart form-data job format to accomodate for each piece.

####General Structure

So, how do we apply this to Forge's file brick? First, let's take a look at our File.Push module.

{
  "brick": "file.push",
  "id": "IDHERE",
  "inputs": {
    "id": {
      "_type": "string",
      "_array": false,
      "_value": null
    },
    "body": {
      "_type": "object",
      "_array": false,
      "_value": null
    },
    "query": {
      "_type": "object",
      "_array": false,
      "_value": null
    },
    "headers": {
      "_type": "object",
      "_array": false,
      "_value": null
    }
  },
  "outputs": {}
}

Quickly going over each of these inputs, since this is a newer brick:

  • id: The Azuqua file system ID that our system will use to push the right file into the external service (in this case, Box)
  • body: The body, or job, that will be the main content that we push up to the service. This represents the actual request that would go to the external service. All other inputs effect how our FS behaves with that request.
  • query: The query we would need to send to the FS. Not needed for this example.
  • headers: The headers we would send to the FS. Not needed for this example. Reminder Any headers you need in your request, would be passed into your body.

As you can see, body, is the most important piece of this equation, so let's take a look at what components we need inside of it, normally:

{
  url: URL string (required),
  protocol: HTTP (required)
  method: HTTP method (required),
  qs: query string (or JSON object representing query string),
  headers: HTTP headers (as a JSON object)
}

However, things are a bit different with multipart requests. This multipart form-data request involves sending separate key/value pairs over an HTTP connection, with FS data being treated as just another piece of the request body. To accomodate for this semantic, the body of a request being sent through the file.push kernel module looks like this:

{
  url: URL string (required),
  protocol: HTTP(required)
  method: HTTP method (required),
  qs: query string (or JSON object representing query string),
  headers: HTTP headers (as a JSON object),
  form-data: form data information (as a string or JSON object),
  form-loc: form data location for FS data
}

There are two new additions here: form-data and form-loc. See below for more details.

form-data This describes form-data key/value pairs that don't contain FS data. Form-data would include things that the request needs to utilize the file correctly (Box examples = parent folder ID in the service, the name it should be saved under, etc). This value can take two forms:

  • object: Contains the following structure:
{
  [key name]: {
    content: raw data to be sent under this key as a string. Can be implied when params/headers are empty. 
    params: disposition parameters, e.g. 'filename' (optional, JSON object),
    headers: extra headers to sent with this piece of form data, e.g. 'content-type' (optional, JSON object)
  },
  // other form-data key/value pairs ...
}
  • string: If a string, form-loc (inside job content) is ignored and this data operates as the key/value pair sent with FS data. Treated as exactly the following:
{
  [form-data value]: {
    content: FS data (attached internally),
    params: {},
    headers: {}
  }
}

As an implementation note, the FS sends these key/value pairs before sending over the internal FS data.

We noted above that Box requests stringified attributes for it's implementation of file upload, so let's see what that looks like in this format (in javascript):

var formData = {
  "attributes": JSON.stringify({
    name: "file_name.txt",
    parent: {
      id: 0 // if we want to insert this file under the root directory
    }
  }),
  // if Box required more form-data parts ...
};

form-loc

Describes the form-data key/value pair containing FS data. This value can take two formats:

  • object: Contains the following structure:
{
  name: name of key to place FS data under ([key name] in 'form-data', string),
  params: disposition parameters, e.g. 'filename' (optional, JSON object),
  headers: extra headers to send with this piece of form data, e.g. 'content-type' (required, if params are present, JSON object)
}
  • string: If a string, treated as exactly follows:
{
  name: [form-loc value],
  params: {},
  headers: {}
}

Continuing with our Box example, here's what this part of the job structure looks like, in Javascript:

var formLoc = {
  name: "file",
  params: {
    "filename": "\"local_file.txt\"" // Box requires a local file name be attached to file content
  },
  headers : {}
};

Note Box requires a local file name be attached to the actual file data being sent with the upload request (and hence why it goes under the "form-loc" key, it's FS data) and we add it as the "filename" disposition parameter. Note We also stringify the local file name as well; form-data requests require the filename parameter be stringified.

Implementation in Javascript Taking all these parts together, we can now construct the entire body of the file.push kernel module, in Javascript, to show how the pieces work together:

var token = "imatoken";
var formLoc = {
  name: "file",
  params: {
    "filename": "\"local_file.txt\"" // Box requires a local file name be attached to file content
  }
};
var formData = {
  "attributes": JSON.stringify({
    name: "file_name.txt",
    parent: {
      id: 0 // if we want to insert this file under the root directory
    }
  })
};

var job = {
  url: "https://upload.box.com/api/2.0/files/content",
  method: "POST",
  headers: {
    Authorization: "Bearer " + token
  },
  "form-data": formData,
  "form-loc": formLoc
};

####Forge

Now that we've seen what the general code expectations of this would be, we can have idea of how to approach this in Forge. Using Box as an example, we know that we need to:

  1. Build our form-data object
  2. Build our form-loc object
  3. Build the body of our upload request
  4. Implement the File.Push module correctly
  5. Render this data to our method's UI

Looking into each one a bit more:

1.Looking at Box's documentation, we know that we need a single key called "attributes" that is an object containing two items, a "name" field that will be used to save the file inside of their service, and a "parent" object that contains an "id" that tells Box which parent folder to save the file under.

  • To do this, I created an object.construct that looked like so:
{
  "brick": "object.construct",
  "id": "ATTRIBUTES",
  "inputs": {
    "name": "{{input.File.Name}}",
    "parent": {
      "id": "{{input.File.Parent Folder ID}}"
    }
  },
  "outputs": {
    "output": {
      "_type": "object",
      "_array": false
    }
  }
}
  • Then I ran it through JSON.Stringify, to stringify that object
  • Then I did a final Object.Construct to build out "attributes" correctly. NOTE I did not need to strictly follow the pattern mentioned above because this "attributes" key does not need headers or params.
{
  "brick": "object.construct",
  "id": "FORM-DATA",
  "inputs": {
    "attributes": "{{STRINGIFIED ATTRIBUTES.output}}"
  },
  "outputs": {
    "output": {
      "_type": "object",
      "_array": false
    }
  }
}

2.To build form-loc, Box requires both a "name" field set to "file" and a parameter set to the local file name, which is stringified.

  • I first used JSON.Stringify to stringify the innermost object.
  • Then I used object.construct to build the form-loc's entire object. NOTE: You must explicitly define your headers. You cannot set it equal to "headers" : {}.
{
  "brick": "object.construct",
  "id": "FORM-LOC",
  "inputs": {
    "name": "file",
    "params": {
      "filename": "{{STRINGIFIED FILENAME.output}}"
    },
    "headers": {
      "_type": "object",
      "_value": {}
    }
  },
  "outputs": {
    "output": {
      "_type": "object",
      "_array": false
    }
  }
}

3.Then we must build the body to send as a request to Box. This matches the format defined inside of "General Structure"

{
  "brick": "object.construct",
  "id": "BODY",
  "inputs": {
    "url": "https://upload.box.com/api/2.0/files/content",
    "protocol": "HTTP",
    "method": "POST",
    "headers": {
      "Authorization": "Bearer {{auth.access_token}}"
    },
    "form-data": "{{FORM-DATA.output}}",
    "form-loc": "{{FORM-LOC.output}}"
  },
  "outputs": {
    "output": {
      "_type": "object",
      "_array": false
    }
  }
}

4.Now we need to set up our File.Push module correctly. NOTE 1: input.File.File Content is the ID the user passes in, that represents the ID inside of Azuqua's file system (surfaced to them in other cards as "File Content"). NOTE 2: You must add in the body object yourself, because the template is currently broken. There will be a fix for this end of next week, hopefully. This represents the response from the external service about your request.

{
  "brick": "file.push",
  "id": "REQUEST",
  "inputs": {
    "id": {
      "_type": "string",
      "_array": false,
      "_value": "{{input.File.File Content}}"
    },
    "body": {
      "_type": "object",
      "_array": false,
      "_value": "{{BODY.output}}"
    },
    "query": {
      "_type": "object",
      "_array": false,
      "_value": {}
    },
    "headers": {
      "_type": "object",
      "_array": false,
      "_value": {}
    }
  },
  "outputs": {
    "body": {
      "_type": "object",
      "_array": false
    }
  }
}

5.Finally, I did a simple Object.Cosntruct to render my data correctly. Like so:

{
  "brick": "object.construct",
  "id": "OUTPUT",
  "inputs": {
    "File": {
      "File ID": "{{prevData.body.pathName}}"
    }
  },
  "outputs": {
    "output": {
      "_type": "object",
      "_array": false
    }
  }
}

####Testing with FS Notes:

  • If you get a file ID, it is only valid for the environment you're testing in (alpha ID's don't work in Beta, etc)
  • With File.Push, your file will automatically be deleted after the transaction is complete. You must get a new File ID every time you test if it completes the File.Push action
  • Errors:
  • If you are getting an error message called "enoent" this means that your file ID is incorrect. It may be because it's already been deleted after a successful push, or it was never a real ID to start with
  • If you are getting an error message called "enostream" that means that the way you are trying to use the File.Push / Pull module is incorrect.

##Import ###General Import

Before getting started, make sure that you have enabled all Connector development tools. Guide here.

Once you're inside the Azuqua Connector builder, click the "Import" button in the right corner of the Azuqua Connector builder toolbar. Doing so will start the process of importing a new Connector or Connector version into the Connector builder tool.

It's important to note that importing inside of the Azuqua Connector builder only processes the provided information into the proper UI. It does not Save, Submit or Deploy a Connector for you. This way, if you make a mistake in the import process at all, you don't overwrite a DRAFT or create a new version of a Connector accidentally.

First, select the Connector file that you would like to import into. You may choose a New Connector file at this point to import a brand new Connector.

On this screen, you now have two items to set. The first is the method of importing. The options are:

  • Copy and Paste
  • Github
  • API Endpoint

Copy and Paste allows you to simply paste in text that will be converted and brought into the UI for you. This is great if you want a simple way to import a Connector, and are trying to import once, or infrequently.

Github allows you to import from a Github repository. You will need to authenitcate (OAuth 2.0) with your account before using this feature.

API Endpoint allows you to request a Swagger or Connector JSON file from a hosted API endpoint, that will be then imported into the Azuqua Connector builder. At this stage you may also choose to "subscribe" to that API endpoint. This means that everytime you enter the Azuqua Connector builder, it will check that API endpoint and see if the hosted file has changed. If it has, you will be prompted to see if you would like to automatically updated your Connector in a variety of ways.

Once you have chosen your import method, select the type of definition you will be importing. The options are:

  • Swagger
  • Connector JSON

Swagger will take in any valid Swagger definition and auto-generate Connector JSON for you. See below for more details.

Connector JSON is the default format all Connectors are built in. When going through the Azuqua Connector builder normally, you will be building a Connector JSON file.

At this point, if you have chosen a Connector JSON file, once you hit "Continue", your Connector will finish importing. If you chose "Swagger", you must configure some settings before the import process can finish.

###Swagger Import

Note: This guide assumes that you have a valid Swagger definition already generated. There are many guides to do this for various programming languages, but the basics can be found here.

If your Swagger file is not valid, the Azuqua Connector builder will throw an error at the import set-up phase and not allow the importing process to continue.

After your import set-up has been configured, Swagger settings will need to be set up next.

First, you will see a list of the security schemas available in your Swagger file. Connectors can only support one Authentication schema, so you will need to select the security schema you would like this Connector to use. Supported types of security schemas are: Basic, OAuth 2.0, and API Key. All other types must be implemented by a Connector developer at this time.

After this stage, you will see a list of methods you may add to this Connector. Only methods that allow the chosen security type will be available to add to your Connector. The names of the methods in this list are generated from the optional "operationId" field in your Swagger file. If this is not present, the tool will generate the name by choosing the method and the route name. For example, "getPetFromId" or "Get /{petId}"

After you've selected the methods you would like to import, if you have chosen a security definition with OAuth 2.0, you will also need to pass in your Connector app's Client ID and Secret to set up an Azuqua client app to your service.

Once this is complete, your Connector has been successfully imported! At this stage you can edit the Connector JSON just like normal. Once you're done with that, save, submit and deploy as usual. Guide here

Make sure to test your new Connector thoroughly!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment