Appmixer OpenAPI Generator

The Appmixer OpenAPI generator provides a tool to automatically generate Appmixer components from the OpenAPI v3 specification.

The tool generates an Appmixer connector from an OpenAPI specification. The spec can be extended with special x-connector-... extensions to either provide details that the OpenAPI spec does not define (but are necessary for the Appmixer connector) or to make the resulting connector more user friendly (automatic pagination, dynamic select with options instead of providing hardcoded values, ...).

Getting started

In the simplest form, you can run the generator by passing an OpenAPI spec as a parameter (both .json and .yaml file formats are accepted) together with a directory where the Appmixer connector will be generated:

$ appmixer init openapi ./examples/openapi/boredapi/openapi.json ./examples/openapi/boredapi/

Outputs:

OpenAPI specification is valid.
No `security` scheme defined in the root level. Authentication module will not be generated.
API name: Bored API, Version: 1.0.0
Statistics: 1 Operations, 1 Paths, 0 Webhooks, Generated Components 1
Auth scheme used: null
Used extensions:

The resulting connector is in the output directory:

$ tree ./examples/openapi/boredapi

boredapi/
├── bundle.json
├── core
│   └── GetActivity
│       ├── GetActivity.js
│       ├── component.json
│       └── package.json
├── openapi.json
├── package.json
└── service.json

This generates a complete Appmixer connector that can be packed and published to an Appmixer tenant:

$ appmixer pack ./examples/openapi/boredapi
$ appmixer publish appmixer.boredapi.zip

Patching the OpenAPI specification

If you use a 3rd party OpenAPI specification and want to make changes to it or enrich it with extensions, it would not be practical to edit this JSON/YAML file directly. Editing the file directly would make it hard to keep track of changes or update the original spec file when needed. To avoid direct editing of the spec, the Appmixer OpenAPI generator accepts a separate JSON Patch file (.json-patch) that defines changes to the OpenAPI spec. This way, the original OpenAPI spec can be left intact and only changes to it (together with extensions) can be defined separately.

The JSON Patch file follows the RFC 6902 (a quick reference) with some small modifications to make it easier to identify values that need to be changed in the OpenAPI spec:

  • The standard JSON Patch file uses JSON Pointer to identify values in a JSON file. However, the JSON Pointer format does not provide the expressive power that one would expect from a format that is to be manually written, especially when identifying multiple places within a JSON file using a single expression.

Consider the following example that removes the clientId parameter from all the parameters in the OpenAPI spec because the clientId is already part of the authentication screens. In other words, we don't need this parameter to be part of the configuration of each Appmixer component (Inspector UI) since we got the value once when the user authenticated to the connector (created a Connection). Since our example OpenAPI spec includes the clientId parameter in all the paramters section for each path item, we want to remove all the occurences. If we were to use the standard JSON Pointer format, we would have to list all the occurences of the clientId inside the parameters section for each path item such as:

{ "op": "remove", "path": "/paths/~1service~1tickets/parameters/2" },
{ "op": "remove", "path": "/paths/~1company~1contacts/parameters/1" },
{ "op": "remove", "path": "/paths/~1user~1emails/parameters/2" },
...

As you can see, this is not exactly user friendly. Instead, the Appmixer OpenAPI generator introduces the jsonpath parameter for operations that accept the JSON Pointer-based path parameter and therefore []allows us to use a single remove operation instead:

{ "op": "remove", "jsonpath": "$..parameters[?(@.name==\"clientId\" && @.in == \"header\")]" }

The jsonpath parameter accepts a JSON Path expression. The Appmixer OpenAPI generator pre-processes the JSN Patch file to expand operations that contain the jsonpath parameter by finding all the values in the JSON OpenAPI spec document that the JSON Path expression points to and multiplying the operation for each value found while replacing the jsonpath with path containing the JSON Pointer expression that identifies the value.

To patch the OpenAPI spec with a JSON Patch file, pass the path to the patch file to the --patch argument:

$ appmixer init openapi ./examples/openapi/zoom/ZoomMeetingAPISpec.json --patch ./examples/openapi/zoom/openapi.json-patch ./zoom

Artifacts

Sometimes, it is useful to be able to see byproducts of the preprocessing of the OpenAPI spec and the patch file. To generate these byproducts, use the --artifacts argument:

$ appmixer init openapi ./examples/openapi/zoom/ZoomMeetingAPISpec.json --patch ./examples/openapi/zoom/openapi.json-patch --artifacts ./zoom

This will create a special directory artifacts/ under the output directory with the following files:

$ tree zoom/artifacts/

zoom/artifacts/
├── checksum.json
├── openapi.json
├── openapi.normalized.json-patch
├── openapi.original.json-patch
└── openapi.patched.json

0 directories, 5 files
  • checksum.json contains a SHA-256 digests for all the files generated by the generator. This is useful for a quick check whether manual changes have been made to any of the generated files or whether a re-generation of the connector produced a different result (possibly due to changes in the OpenAPI spec).

  • openapi.json is the original OpenAPI spec file.

  • openapi.normalized.json-patch is the JSON Patch file withe "normalized" jsonpath attributes. See Patching OpenAPI specification.

  • openapi.original.json-patch is the original JSON Patch file.

  • openapi.patched.json is the final OpenAPI spec, normalized, dereferenced (i.e. with resolved $ref references) and possibly patched.

Array vs Single Object Outputs

The Appmixer OpenAPI generator automatically checks for the operation success response JSON schema and if it detects type: array, it adds an "Output Options" select box to the resulting generated component configuration. This allows the end-user to select, whether they are interested in outputting all items at once (array output) or one item at a time (object output). The same logic applies when the x-connector-pagination extension is defined on the operation.

OpenAPI specification to Appmixer Connector mapping

The following table describes how the OpenAPI specification fields/features are mapped to Appmixer connectors.

OpenAPI field/feature
Appmixer Connector Field/Feature

info.title

Service name (lower cased with spaces removed). Can be customized using x-connector-service

info.description

Service description

info.title

Bundle changelog version description

servers[0].url

The first server is used as the base URL for all HTTP calls in the connector. Other servers are ignored.

servers[0].variables

For apiKey based authentication schemes, server variables are requested by the end-user in the authentication flow (i.e. considered in the auth.js form and later used to construct the base URL). Only the first server variables are used. Other servers are ignored.

paths

Each Operation Object is converted to an Appmixer component. The path of the Path Object is used to construct the URL to be called (using the HTTP method from the Path Item Object) by the component as part of its behaviour.

paths...operationId

The operation ID is used as the Appmixer component name after normalization. The normalization consists of camel casing the operation ID if it contains the . (dot) character since the dot character can't be used in the Appmixer component name. Otherwise, the operation ID is used as is. if the operation ID is missing in the OpenAPI spec, the Appmixer component name is constructed from the HTTP method and path (e.g. GET /user) becomes GetUser.

paths...responses

The first JSON-like success response object is heuristically found and used to construct the output of the component. Only one out output port is always created and the response JSON schema is used as to construct the options object of the output port (including JSON schemas for nested objects and arrays). The heuristics to find the first JSON-like response object finds the first response with 2xx HTTP Status Code and Media Type containing the "json" string.

paths...requestBody

The request body object is used to construct the input of the component (together with parameters). Only the following media types are supported: applicatoin/json, application/x-www-form-urlencoded and multipart/form-data. The input of the component is flattened and all nested properties of the original request body are delimited using the `'

paths...parameters

The parameters are used to construct the input of the component (together with requestBody).

File Uploads

Input fields that are of the JSON schema type string and format binary are considered binary file inputs. These input fields are mapped to the Appmixer filepicker inspector field type. For such inputs, the Appmixer OpenAPI generator automatically generates component code that loads the file from the Appmixer Files storage and streams it to the API endpoint. The multipart-form-data; boundary=FORM_BOUNDARY Content-Type HTTP header is automtically set on these requests.

Selecting Operations for Conversion

When using an external OpenAPI spec that you do not want to edit, use the JSON Patch with the following operation to select OpenAPI operations that you want to convert to Appmixer components:

{ "op": "remove", "jsonpath": "$.paths.*[?(@.operationId != 'meetings' && @.operationId != 'meetingCreate')]" }

In the example above, only the meetings and meetingCreate operations will be considered by the OpenAPI generator for conversion.

Supported Authentication Types

Only OAuth 2 and API key based (with keys in all query/header/cookies) authentication is supported. The generated authentication module can be controlled with the x-connector-connection, x-connector-connection-check and x-connector-connection-profile extensions.

OpenAPI Extensions

The Appmixer OpenAPI generator accepts multiple extensions that can be used to define constructs that the standard OpenAPI specification does not provide. Without these constructs, the final generated connectors would not be as user friendly.

x-connector-version

Description

Define the version of the connector. The connector is versioned using a major.minor.patch versioning scheme. The major.minor portion of the version string (for example 3.1) shall designate the connector feature set. .patch versions address errors in, or provide clarifications to, the connector, not the feature set.

Note that, unfortunately, we can't use info.version as the connector version since info.version can be an abitrary string while the connector REQUIRES the use of semantic versioning.

Location

  • Info Object

Value

A version string using the major.minor.patch versioning scheme. If not provided, the default value is 1.0.0.

Example

"info": {
  ...
  "x-connector-version": "2.1.1"
  ...
}

x-connector-icon

Description

Define an icon for the connector.

Location

  • Info Object

Value

A URL to an image (png, jpeg or SVG). A Data URI is also accepted.

Example

"info": {
  ...
  "x-connector-icon": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
  ...
}

Description

Alternatively to the x-connector-icon, the Appmixer OpenAPI generator also accepts the more common x-logo extension.

Location

  • Info Object

Value

Either a URL to an image (png, jpeg or SVG; with data URI also accepted) or an object with a url field that points to an image.

Example (URL directly)

"info": {
  ...
  "x-logo": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
  ...
}

Example (nested url field)

"info": {
  ...
  "x-logo": {
      "url": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
      ...
  }
  ...
}

x-connector-service

Description

Define a name for the Appmixer connector service of the service/module/component hierarchy.

Location

  • Info Object

Value

A string. If not provided, the default value is the info.title in lower case with removed spaces.

Example

"info": {
  ...
  "x-connector-service": "jotform"
  ...
}

x-connector-module

Description

Define a name for the Appmixer connector module of the service/module/component hierarchy.

Location

  • Info Object

Value

A string. Deault value is core.

Example

"info": {
  ...
  "x-connector-module": "forms"
  ...
}

x-connector-connection-check

Description

An HTTP request that will be called to validate whether the user authentication credentials (Connection) are valid. In the most common sense, this request is called to check whether the api key or OAuth access token is valid. Typically, this is a call to a /me-type of endpoint that is sent with authentication details included (OAuth access token, api key, ...). The check is valid if the response is a success (HTTP 2xx status code).

Location

  • Security Scheme Object

Parameters

  • method ... HTTP method (GET, POST, ...)

  • url ... The endpoint to be called. If the URL is relative, it will be relative to the base URL (defined in the OpenAPI servers section).

  • headers ... HTTP headers object.

  • query ... HTTP query object (key-value pairs representing the query string parameters).

The values of the url, headers, and query parameters can contain parameters enclosed in curly braces. These parameters will be replaced by the values collected from the user during the authentication flow (the named parameters in the Security Scheme Object) or are implicitely provided (accessToken in case of OAuth).

Examples

A common use in API keys-based authentication:

"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-check": {
                "method": "GET",
                "url": "/user?apiKey={apiKey}"
            },
            ...
        }
    },

A common use in Oauth-based authentication:

"components": {
    "securitySchemes": {
      "OAuth": {
        "x-connector-connection-check": {
          "method": "GET",
          "url": "/users/me",
          "headers": {
              "Authorization": "Bearer {accessToken}"
          }
        },
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://zoom.us/oauth/authorize",
            "scopes": {},
            "tokenUrl": "https://zoom.us/oauth/token"
          }
        },
        "type": "oauth2"
      }
    }
}

x-connector-connection-profile

Description

An HTTP request that will be called to get the user profile (used mainly to get a display name for the Connection). Typically, this is a call to a /me-type of endpoint that is sent with authentication details included (OAuth access token, api key, ...). The returned value is then used to get the display name for the Connection. Alternatively, it can also be a string that can contain parameters in curly braces. For security schemes with "type": "apiKey", the {apiKey} paramater will be replaced with the API key provided by the user.

Location

  • Security Scheme Object

Parameters

  • method ... HTTP method (GET, POST, ...)

  • url ... The endpoint to be called. If the URL is relative, it will be relative to the base URL (defined in the OpenAPI servers section).

  • headers ... HTTP headers object.

  • query ... HTTP query object (key-value pairs representing the query string parameters).

  • transform ... JSONata expression to retrieve the value from the response payload that will be used as a display name for the Connection.

The values of the url, headers, and query parameters can contain parameters enclosed in curly braces. These parameters will be replaced by the values collected from the user during the authentication flow (the named parameters in the Security Scheme Object) or are implicitely provided (accessToken in case of OAuth).

Examples

A common use in API keys-based authentication:

"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-profile": {
                "method": "GET",
                "url": "/user?apiKey={apiKey}",
                "transform": "content.name"
            },
            ...
        }
    },

A common use in API keys-based authentication with no /me endpoint:

"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-profile": "{apiKey}"
            ...
        }
    },

A common use in Oauth-based authentication:

"components": {
    "securitySchemes": {
      "OAuth": {
        "x-connector-connection-profile": {
          "method": "GET",
          "url": "/users/me",
          "headers": {
              "Authorization": "Bearer {accessToken}"
          },
          "transform": "email"
        },
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://zoom.us/oauth/authorize",
            "scopes": {},
            "tokenUrl": "https://zoom.us/oauth/token"
          }
        },
        "type": "oauth2"
      }
    }
}

x-connector-connection

Description

For API-key based authentication, the x-connector-connection extension is an alternative to the OpenAPI-native definition. The x-connector-connection is more compact and contains all the necessary parameters for the entire Connection definition (including checks and profile requests). Also, the standard OpenAPI apiKey security scheme does not have a mechanism to describe how exactly is the api key passed in the request. It only allows to define where it is passed (header, query). However, some APIs require the value of the api key parameter to be more than just the key. For example, the OpenAI API uses the following in the HTTP headers: { Authorization: "Bearer {apiKey}" }. The Voys API even combines more api keys into one with: { Authorization: "token {username}:{apiKey}" }. If the x-connector-connection extension is used, it overrides other authentication definitions from the security section.

Location

  • Root.

Parameters

  • type ... The type of the authentication scheme. Only "apiKey" is currently accepted.

  • in ... The location of the authentication credentials (api key(s)) in all authentication requests. Can be "header", "query" or "cookie".

  • name ... Name of the header, query parameter or cookie.

  • value ... The template for the value of the authentication credential. It can use any property from the schema object enclosed in curly brackets. These will be replaced with real values at runtime.

  • schema ... JSON schema describing the authentication parameters. The schema is used to generate the authentication screen for the user where the parameter values will be requested from the user in a HTML form. Only a flat object type of JSON-schema structure is supported at this point and only the string type is supported for the properties.

  • check ... An HTTP request that will be called to validate whether the user authentication credentials (Connection) are valid. The same parameters as in the x-connector-connection-check extension are supported.

  • profile ... An HTTP request that will be called to get the user profile (used mainly to get a display name for the Connection). The same parameters as in the x-connector-connection-profile extension are supported. Alternatively, the profile can also be a template string that can contain parameters defined in the schema in curly brackets.

Examples

Two API keys (one username, one api key, custom value for Authorization header)

"x-connector-connection": {
    "type": "apiKey",
    "in": "header",
    "name": "Authorization",
    "value": "token {username}:{apiKey}",
    "schema": {
        "type": "object",
        "properties": {
            "username": {
                "type": "string",
                "title": "Username"
            },
            "apiKey": {
                "type": "string",
                "title": "API Key"
            }
        }
    },
    "check": {
        "method": "GET",
        "url": "/callnotification/callnotification/",
        "headers": {
            "Authorization": "token {username}:{apiKey}"
        },
        "expect": {
            "status": 405
        }
    },
    "profile": "{username}"
},

One API key (custom value for authorization header)

"x-connector-connection": {
  "type": "apiKey",
  "in": "header",
  "name": "Authorization",
  "value": "Bearer {apiKey}",
  "schema": {
      "type": "object",
      "properties": {
          "apiKey": {
              "type": "string",
              "title": "API Key",
              "description": "Log into your OpenAI account and find your API key."
          }
      }
  },
  "check": {
      "method": "GET",
      "url": "/models",
      "headers": {
          "Authorization": "Bearer {apiKey}"
      }
  },
  "profile": "{apiKey}"
}

x-connector-label

Description

A custom label for the generated Appmixer component. By default, the label of the component is the operationId defined in the Operation Object.

Location

  • Operation Object

Value

A string.

Example

...
operationId: 'postContacts',
x-connector-label: 'CreateContact'
...

x-connector-description

Description

A custom description for the generated Appmixer component. By default, the description of the component is of the form <label>summary</label></br>description.`

Location

  • Operation Object

Value

A string.

Example

...
x-connector-description: 'Create a new contact or update an existing one.'
...

x-connector-field-index

Description

In OpenAPI specification, parameters and requestBody fields are typically not ordered by their importance from the perspective of a user filling a form that contains those fields. Therefore, in many cases, required fields or important fields may end up at the bottom of such a form. This is obviously not a great user experience. The x-connector-field-index allows you to give fields an order that you'd prefer when the fields are rendered in a form for the user to configure (Appmixer Inspector panel).

Location

  • Schema Object

Value

An integer.

Example (parameters)

...
"parameters": {
    "in": "query",
    "name": "accountId",
    "required": true,
    "schema": {
        "type": "string",
        "x-connector-field-index": 1
    }
...

Example (requestBody)

...
"requestBody": {
    "content": {
        "application/json": {
            "schema": {
                "type": "object",
                "properties": {
                    "firstName": {
                        "type": "string",
                        "x-connector-field-index": -1
                    }
                }
            }
        }
    }
}
...

x-connector-field-options

Description

Since not all options can be defined using the JSON schema, the x-connector-field-options makes it possible to add additional options to the generated Appmixer inspector field. Also note that the field options are processed after all other heuristics used to automatically convert JSON schemas to Appmixer inspector field took place. Therefore, if the generated inspector field is not what you expect, you can use the x-connector-field-options to override the generated setting.

Location

  • Schema Object

Value

An object.

Example

...
"requestBody": {
    "content": {
        "application/json": {
            "schema": {
                "type": "object",
                "properties": {
                    "start_time": {
                        "type": "string",
                        "format": "date-time",
                        "x-connector-field-options": {
                            "format": "MM-ddTHH:mm:ss"
                        }
                    }
                }
            }
        }
    }
}
...

x-connector-transform

Description

Location

  • Request Body Object

Parameters

  • language ... the language of the expression. Currently only JavaScript is supported.

  • expression ... the expression to transform the request body. The request body is available with the requestBody variable inside the expression. For example, the expression requestBody = {} empties the request body object completely, ignoring everything the user has set in the inspector.

Example

...
"requestBody": {
    "content": {
        "application/json": {
            "x-connector-transform": {
                "language": "javascript",
                "expression": "requestBody.start_time = requestBody.start_time.replace('Z', ''); requestBody.timezone = requestBody.timezone || 'UTC';",
            },
            "schema": {
                "type": "object",
                "properties": {
                    "start_time": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            }
        }
    }
}
...

x-connector-source

Description

Turn a property into a select box with options loaded from another source.

Location

  • Schema Object (Any property in either requestBody or parameters).

Parameters

  • operationId ... the ID of the operation within the OpenAPI spec that will be called to retrieve the data for the options of the select box. The resulting data will be read from the out output port.

  • transform ... A JMESPath expression that must transform the resulting data into an array of objects with value and label fields. The label field is the visible name of the option to the user. The value field is the value that what will be used as the value for the field for which the x-connector-source extension is defined. [Note that JMESPath is used instead of JSONata - prevalent in other extensions - since the Appmixer tranform functions cannot be currently asynchronous.]

  • parameters ... [optional] A map of parameters that will be propagated to the source operation. An object with keys representing parameters of the source operation and values that are JSON pointers pointing to data of the operation that will propagate to the source operation. The root of the JSON pointer points to the operation under which the x-connector-source extension is defined.

  • requestBody ... [optional] A map of request body parameters that will be propagated to the source operation. An object with keys representing request body parameters (nested parameters are expressed using the / character) of the source operation and values that are JSON pointers pointing to data of the operation that will propagate to the source operation. The root of the JSON pointer points to the operation under which the x-connector-source extension is defined.

Example (typical use)

...
"requestBody": {
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "board": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "x-connector-source": {
                  "operationId": "getServiceBoards",
                  "transform": "result[].{value: id, label: name}"
                }
              }
            }
          }
        }
      }
    }
  }
}
...

Consider an OpenAPI specification for Connectwise (examples/openapi/connectwise/openapi.json) for an endpoint that updates a service ticket (putServiceTicketsById). This endpoint takes board.id parameter in the request body to identify the service board the ticket should be placed in. If we left the OpenAPI spec without any modifications, the generated component would request the board.id from the user on an as-is basis, i.e. it would ask the user to provide the ID of the service board in a text input field. As you can imagine, this is not very helpful to the user since the user does not know that value. One way for the user to get around this would be to use another component such as getServiceBoards to search all the service boards and use the id output of the board found as an input of our board.id. However, in this case, it is much more user friendly to just show a list of service board names to the user right inside our putServiceTicketsById component and use the selected board's id automatically without the user having to deal with plain IDs.

Example (parameters propagation)

Sometimes, the source operation needs some parameters that are defined by the user in the design phase. To specify which parameters are propagated to the source operation, define the parameters and requestBody sections. In the example below, the userId is a parameter of the source operation and { form: { type: ... } } is part of the request body of the source operation. Assume that both parameters are required and without which the source component cannot return any results (and fails for missconfiguration). We can simply provide a mapping of the parameters and request body data that will be propagated to the source component like this:

                        "x-connector-source": {
                            "operationId": "GetUserForms",
                            "transform": "result[].{value: id, label: title }",
                            "parameters": {
                                "userId": "/parameters/userId"
                            },
                            "requestBody": {
                                "form/type": "/requestBody/content/application~1json/schema/properties/formType"
                            }
                        }

x-connector-pagination

Description

Define a pagination method. This is especially useful for components that call endpoints that return an array of values (as opposed to a single object). In such cases, we may want to let the user configure how many items to return. At the same time, this limit on the number of items can be higher than the endpoint allows to return in one call (i.e. higher than the common limit paramater often used in the offset-limit type of pagination). If the x-connector-pagination is defined, the generated component will be smart enough to call the endpoint with varying parameters multiple times to retrieve the desired number of items.

Location

  • Operation Object

Parameters

  • type ... the type of the pagination used. One of "page", "cursor", "link-header" and "once".

  • parameters ... the parameters of the pagination specific to each type.

page type pagination parameters

  • offset ... The name of the query parameter of the 3rd party endpoint that represents the (zero-based) offset of the first item returned in the collection.

  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in one call of the endpoint. This number will be added to the offset parameter whith each subsequent call of the endpoint.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

  • count ... [optional] A JSONata expression that is evaluated on the response data that returns the total number of items in the collection. This is a hint to the component to stop calling the endpoint when all results have been received. The most common value just points to the field from the response data that contains the total count of items. Example: resultSet.count.

  • more ... [optional] A JSONata expression that is evaluated on the response data that returns a boolean value that tells teh component to stop calling the endpoint in order to get more results. In other words, if more evaluates to false, the component knows it has collected all the results. The most common value points to the boolean field from the response data that contains informatoin on where there are more items. Example: hasMore.

"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "page",
            "parameters": {
                "offset": "offset",
                "limit": "limit",
                "page": 20,
                "results": "content",
                "count": "resultSet.count"
            }
        }
        ...
    }
}

cursor type pagination parameters

  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in one call of the endpoint.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

  • next ... A JSONata expression that is evaluated on the response data that should return the value of the cursor, i.e. the next cursor. This is typically the ID of the next item starting from which we want retrieve the next batch of results.

  • cursor ... The name of the query parameter of the 3rd party endpoint that represents the cursor.

"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "cursor",
            "parameters": {
                "limit": "limit",
                "page": 20,
                "results": "content",
                "cursor": "since",
                "next": "next_object"
            }
        }
        ...
    }
}

link-header type pagination parameters

This pagination expects the response to contain the Link HTTP header that contains at least one URL that, if requested, returns the next batch of items. This URL must have the rel="next" parameter set. For example:

Link: <https://api.github.com/repositories/1300192/issues?page=2>; rel="prev", <https://api.github.com/repositories/1300192/issues?page=4>; rel="next", <https://api.github.com/repositories/1300192/issues?page=515>; rel="last", <https://api.github.com/repositories/1300192/issues?page=1>; rel="first"
  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in the first call of the endpoint. (Subsequent calls are assumed to have this parameter automatically added by the server returning the next URL in the Link HTTP header.)

"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "link-header",
            "parameters": {
                "limit": "limit"
            }
        }
        ...
    }
}

once type pagination parameters

This special type of pagination makes it easy to introduce paginated "features" (Limit inspector field, result output variable) on endpoints that do not support pagination but return an array of items. This is typically the case for endpoints that, even though return an array of items, do not return "much" of the items so no pagination is defined and necessary.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

"/user/reports": {
    "get": {
        "summary": "Get User Reports",
        "operationId": "GetUserReports",
        "x-connector-pagination": {
            "type": "once",
            "parameters": {
                "results": "content"
            }
        }
        ...
    }
}

x-connector-webhook

Description

Define a webhook trigger. Two common types of webhooks are supported:

  • A "subscription" type of webhook requires the 3rd party API to provide two endpoints for dealing with webhooks: subscribe and unsubscribe. The "subscribe" endpoint accepts a URL that will be called by the 3rd party to notify of new events occuring. The "unsubscribe" endpoint makes it possible to tell the 3rd party to remove the subscribed webhook from its registry and therefore stop receiving events on that URL.

  • A "static" type of webhook allows to manually register a global webhook URL with the 3rd party that the 3rd party will notify of new events occuring. In this case, both the subscribing and unsubscribing of the webhook is done manually (usually as part of the OAuth app configuration).

Location

  • Path Item Object of the webhooks section.

Parameters

type="subscription" webhook parameters

  • type ... The type of the webhook. For the subscription type, use "subscription".

  • subscribe ... An object defining the HTTP request to be sent to subscribe a webhook with the 3rd party to receive events to.

  • subscribe.url ... The URL of the request.

  • subscribe.method ... The HTTP method of the request.

  • subscribe.headers ... The HTTP headers of the request.

  • subscribe.body ... The data payload of the request.

  • unsubscribe ... An object defining the HTTP request to be sent to unsubscribe a webhook from the 3rd party in order to stop receiving events to the previously subscribed URL.

  • unsubscribe.url ... The URL of the request. A special template parameters can be used to

  • unsubscribe.method ... The HTTP method of the request.

  • unsubscribe.headers ... The HTTP headers of the request.

  • unsubscribe.body ... The data payload of the request.

  • outputCondition ... [optional] A JSONata expression that must evaluate to true in order for the trigger to output data in reaction to an incoming event that arrives at the webhook URL.

  • outputTransform ... [optional] A JSONata expression that can be used to transform the output of the trigger. The expression evaluates on the incoming request payload.

Special placeholders can be used in the url, headers and body fields of the subscribe and unsubscribe parameters. These will be replaced at runtime and include:

  • {$baseUrl} ... This will be replaced by the base URL of the 3rd party service (see the servers section of the OpenAPI specification). Usable both in subscribe and unsubscribe objects.

  • {$webhookUrl} ... The Webhook URL of the trigger component. Usable both in subscribe and unsubscribe objects.

  • {$request.body} ... The entire request body. See OpenAPI runtime expressions for more details. Usable mainly in the output parameter.

  • {$response.body#/foo/bar} ... A portion of the response body specified by a JSON Pointer. See OpenAPI runtime expressions for more details. Usable mainly in the unsubscribe object to point to fields from the payload returned in the response to the subscribe endpoint call. Typically, the response contains the ID of the webhook which we want to use in the request to unsubscribe from the webhook later on.

  • {$response.transform#JSONATA_EXPRESSION} ... The response transformed using the JSONata expression language. This gives you the most expressive power - if needed. Sometimes, the {$response.body#/foo/bar} that uses the JSON Pointer does not have enough expressive power to extract the ID of the webhook to unsubscribe. In these cases, use this parameter together with a JSONata expression to query the data you need to unsubscribe the webhook.

  • {$response.header.header_name} ... The value of the specified response header. See OpenAPI runtime expressions for more details.

  • {$parameters.parameter_name} ... The value of the specified parameter from the parameters section of the OpenAPI operation. parameters make it possible to parametrize the webhook trigger (request information from the user). Use the {$parameters.parameter_name} placeholder to reference the parameter values in both the subscribe and unsubscribe objects.

  • {$connection.profile#/foo/bar} ... A portion of the connection profile object (see x-connector-connection-profile). This is especially useful to get values for data such as user ID, account ID or other, that are returned from the /me-type of endpoint. Use JSON Pointer after the # character to get to any nested values.

Example: using of parameters, output transformation

"webhooks": {
    "NewContact": {
        "post": {
            "x-connector-webhook": {
                 "type": "subscription",
                 "subscribe": {
                    "url": "{$baseUrl}/system/callbacks",
                    "method": "POST",
                    "body": {
                        "url": "{$webhookUrl}",
                        "objectId": "{$parameters.objectId}",
                        "type": "Contact",
                        "level": "{$parameters.level}"
                    }
                },
                "unsubscribe": {
                    "url": "{$baseUrl}/system/callbacks/{$response.body#/id}",
                    "method": "DELETE",
                    "body": null
                },
                "outputCondition": "$boolean(Action='added')",
                "outputTransform": "$eval(Entity)"
            },
            "operationId": "NewContact",
            "summary": "Triggers when a new company contact was created.",
            "parameters": [{
                "name": "level",
                "in": "body",
                "description": "When set to owner, all ConnectWise PSA contacts are returned. When set to type, all contacts of the specified type are returned. When set to territory, all contacts of the specified territory are returned. When set to company, all contacts of the specified company are returned. When set to contact, the specified contact is returned.",
                "required": false,
                "schema": {
                    "type": "string", "enum": ["Owner", "Type", "Territory", "Company", "Contact"],
                    "default": "Owner"
                }
            }, {
                "name": "objectId",
                "in": "body",
                "description": "The ObjectId should be the Id of whatever record you are subscribing to. This should be set to 1 when using a level of Owner.",
                "required": false,
                "schema": { "type": "integer", "default": 1 }
            }],
            "requestBody": {
                "description": "New Contact Created",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Contact"
                        }
                    }
                }
            },
            "responses": {
                "200": {
                    "application/json": {}
                }
            }
        }
    }
}

Example: a more complex unsubscribe URL with transforms

"webhooks": {
    "NewFormSubmission": {
        "post": {
            "operationId": "NewFormSubmission",
            "summary": "Triggers when a form is submitted.",
            "x-connector-webhook": {
                "type": "subscription",
                "subscribe": {
                    "url": "{$baseUrl}/form/{$parameters.formId}/webhooks",
                    "method": "POST",
                    "headers": { "Content-Type": "application/x-www-form-urlencoded" },
                    "body": { "webhookURL": "{$webhookUrl}" }
                },
                "unsubscribe": {
                    "url": "{$baseUrl}/form/{$parameters.formId}/webhooks/{$response.transform#$keys(data.content) ~> $filter(function ($key) { $lookup(data.content, $key) = \"{$webhookUrl}\"})}",
                    "method": "DELETE",
                    "body": null
                },
                "outputCondition": "$exists(submissionID)"
            },
            "parameters": [{
                "name": "formId",
                "in": "body",
                "description": "Form ID",
                "required": true,
                "schema": {
                    "type": "string",
                    "x-connector-source": {
                        "operationId": "GetUserForms",
                        "transform": "result[].{value: id, label: title }"
                    }
                }
            }],
            "requestBody": {
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/WebhookSubmission"
                        }
                    }
                }
            },
            "responses": {
                "200": {
                    "content": {
                        "application/json": {
                        }
                    }
                }
            }
        }
    }
}

type="static" webhook parameters

  • type ... The type of the webhook. For the static type, use "static".

  • path ... The endpoint of the static webhook. The entire URL will be {APPMIXER_API_URL}/plugins/appmixer/{SERVICE}{PATH}. For example: https://api.appmixer.com/plugins/appmixer/zoom/events.

  • pattern ... A JSONata expression that is evaluated on the incoming webhook request (with payload, headers, method and query fields). If the topic parameter matches the evaluated pattern, the component triggers and outputs the incoming request payload (considering outputCondition is met).

  • topic ... The value of the evaluated pattern for which the component is interested in receiving events.

  • outputCondition ... [optional] A JSONata expression that must evaluate to true in order for the trigger to output data in reaction to an incoming event that arrives at the webhook URL.

  • outputTransform ... [optional] A JSONata expression that can be used to transform the output of the trigger. The expression evaluates on the incoming request payload.

  • crc ... The definition of the Challenge-Response check. Some APIs use the challenge-response check to confirm the ownership and the security of the webhook notification endpoint URL. In this scenario, the API sends a POST request to the webhook URL with a challenge (a token) and the webhook endpoint must return the challenge hashed with a secret token known to both sides.

  • crc.condition ... A JSONata expression evaluated on the incoming request (with payload, headers, method and query fields) that must evaluate to true for the incoming request to be considered as the challenge request (i.e. to ignore all non-CRC requests).

  • crc.alg ... The hash function used to calculate HMAC. Typically "sha256".

  • crc.key ... The name of the configuration value of the connector that contains a secret token known to both sides, that will be passed to the HMAC to calculate the final token for the response. This points to the configuration key that can be set in the Appmixer Backoffice -> Configuration for the service appmixer:SERVICE.

  • crc.challenge ... A JSONata expression evaluated on the request returning the challenge token (that will be used together with the key to calculate the HMAC).

  • crc.digest ... The encoding of the HMAC. Supported values are "hex", "base64" and "utf8".

  • crc.response ... A JSONata expression evaluated on an object of the form { responseToken, challenge } containing both the original challenge and the final calculated HMAC (responseToken). The expression should return the payload to be send as a response to the CRC request.

{
  "webhooks": {
    "meeting.deleted": {
      "post": {
        "operationId": "meetingDeleted",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payload|object": {
                    "type": "object",
                    "description": "Information about the meeting.",
                    "properties": {
                      "uuid": {
                        "type": "string",
                        "description": "The meeting's universally unique identifier (UUID)."
                      },
                      "topic": {
                        "type": "string",
                        "description": "The meeting's topic."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-connector-webhook": {
          "type": "static",
          "path": "/events",
          "pattern": "payload.event & ':' & payload.payload.account_id",
          "topic": "meeting.deleted:{$connection.profile.account_id}",
          "crc": {
            "condition": "payload.event = \"endpoint.url_validation\"",
            "alg": "sha256",
            "key": "webhookSecretToken",
            "challenge": "payload.payload.plainToken",
            "response": "{ \"encryptedToken\": responseToken, \"plainToken\": challenge }",
            "digest": "hex"
          }
        }
      }
    }
  }
}

cursor type pagination parameters

  • limit ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.

  • page ... The number of items to return in one call of the endpoint.

  • results ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example content.items.

  • next ... A JSONata expression that is evaluated on the response data that should return the value of the cursor, i.e. the next cursor. This is typically the ID of the next item starting from which we want retrieve the next batch of results.

  • cursor ... The name of the query parameter of the 3rd party endpoint that represents the cursor.

"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "cursor",
            "parameters": {
                "limit": "limit",
                "page": 20,
                "results": "content",
                "cursor": "since",
                "next": "next_object"
            }
        }
        ...
    }
}

Description

Define a URL base path for any link with a relative URL in the connector. Applies only to relative URL links in markdown formatted descriptions. When provided, it will convert [link text](/documentation#endpoints-price-range) to <a href="https://www.boredapi.com/documentation#endpoints-price-range">link text</a>.

Location

  • Info Object

Value

A string.

Example

"info": {
  ...
  "x-connector-rel-link-base-url": "https://www.boredapi.com"
  ...
}

Examples

BoredAPI

  • A minimalistic OpenAPI spec for the single-endpoint https://www.boredapi.com/ API.

  • No JSON patch, no OpenAPI extensions are used.

  • OpenAPI Spec

appmixer init openapi ./boredapi/openapi.json ./connector/

Jotform

  • A custom written single OpenAPI spec with extensions right inside the OpenAPI document.

  • Pagination, connection check/profile, source, custom API-key based authentication, webhooks.

  • OpenAPI Spec

Used extensions:

  • x-connector-icon

  • x-connector-service

  • x-connector-module

  • x-connector-webhook

  • x-connector-source

  • x-connector-pagination

  • x-connector-connection-check

  • x-connector-connection-profile

Zoom

  • OpenAPI with a separate JSON Patch file with changes and fixes, separate OpenAPI spec for webhook definitions externally referenced from the OpenAPI spec.

  • File upload (multipart/form-data), OAuth 2, pagination (cursor), inspector field index, request body JavaScript transformation.

  • OpenAPI Spec

  • OpenAPI Webhooks Spec

  • JSON Patch

appmixer init openapi ./zoom/ZoomMeetingAPISpec.json --patch ./zoom/openapi.json-patch --artifacts ./connector

Used extensions:

  • x-connector-icon

  • x-connector-service

  • x-connector-module

  • x-connector-webhook

  • x-connector-pagination

  • x-connector-connection-check

  • x-connector-connection-profile

  • x-connector-field-index

  • x-connector-transform

OpenAI

  • OpenAPI in YAML format with a separate JSON Patch file with changes and fixes.

  • File upload (multipart/form-data), custom API key-based auth, source, pagination (once), inspector field index.

  • OpenAPI Spec

  • JSON Patch

appmixer init openapi --artifacts --patch ./openai/openapi.json-patch ./openai/openapi.yaml ./connector

Used extensions:

  • x-connector-icon

  • x-connector-service

  • x-connector-module

  • x-connector-pagination

  • x-connector-connection

  • x-connector-field-index

  • x-connector-source

Supported OpenAPI versions

All versions starting from version 2 (Swagger) is supported, i.e. 2.0, 3.0.x and 3.1. Note that the 2.0 version is automatically detected and a conversion to version 3.0.x is automatically performed using the swagger2openapi npm package.

Also note that the tool accepts both YAML and JSON files as inputs for the OpenAPI specification.

Notes & Limitations

  • Fields in parameters and requestBody are mixed together to form a flat list of configuration fields of the connector. Conflicting names are not accepted at this point.

  • Only the first subschema of the oneOf JSON schema composition keyword is used.

  • allOf subschemas are merged into one using the (json-schema-merge-allof)[https://www.npmjs.com/package/json-schema-merge-allof] library.

  • Only the first subschema of the anyOf JSON schema composition keyword is use.d

  • Only the first subschema of the oneOf JSON schema composition keyword is used.

  • The output JavaScript code is formatted using (ESLint)[https://eslint.org/].

\

Last updated