# OpenAPI Connector Generator

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](https://datatracker.ietf.org/doc/html/rfc6902/) ([a quick reference](https://jsonpatch.com/)) 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](https://datatracker.ietf.org/doc/html/rfc6901/) 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](https://goessner.net/articles/JsonPath/). 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"
  ...
}
```

#### x-logo

**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](https://jmespath.org/) 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](https://docs.appmixer.com/appmixer/component-definition/manifest/properties#properties.source.data.transform) 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](https://swagger.io/docs/specification/links/) 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](https://swagger.io/docs/specification/links/) 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](https://swagger.io/docs/specification/links/) 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"
            }
        }
        ...
    }
}
```

#### x-connector-rel-link-base-url

**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](https://github.com/Mermade/oas-kit/blob/main/packages/swagger2openapi/README.md) 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/>].

<br>
