This documentation serves as a baseline for all products that Managed API exists for, but product-specific documentation should be read as well as variations may exist, e.g., error-handling differences from product to product.

What Managed API Tries to Be

  • Managed API tries to be a set of abstractions to increase the user experience when working with third-party APIs. User experience is boosted by providing TypeScript type definitions for third-party API requests and responses so the editor could assist the end-user without expecting the end user to fetch that information from external (third-party) documentation, thus increasing productivity and reducing frustration.
  • Managed API tries to offer built-in discoverability to reduce the need to rely on external (third party) documentation.
  • Managed APIs across different products try to be as consistent as much as meaningfully possible, but to a certain extent only.
  • Managed API tries to provide different abstraction levels, to make sure end users have a choice. Whenever working on the highest level, which is the most opinionated one, doesn't meet end-user expectations they can always drop a level or two lower.
  • On higher abstraction levels Managed API tries to perform automatic HTTP status code validation and automatic error handling, but the actual erroneous response is still the original one that is originating from the actual product.

What Managed API Tries Not to Be

  • Managed API tries to be a relatively thin set of abstractions. Managed API tries not to come up with a singe unified API that abstracts away all the inner workings of underlying APIs, but aims to provide consistent access patterns, IntelliSense for data that needs to be sent (request) and data that is coming back (response), and automatic response error validation.
  • Managed API tries not to pre-process request or post-process responses for unification reasons, data that needs to be sent and data that is coming back is almost always one to one match when compared to original (product level) API. Exceptions being some helper functions that are built on top of existing abstractions to make working with certain resources more easy, for example Jira Issues, but these are exceptions and are only implemented on case by case basis.

About the Asynchronous Nature of JavaScript

JavaScript is single threaded non-blocking asynchronous language. Meaning that anything that cannot be executed right away, such as HTTP calls, because HTTP calls need to wait for a response from the server, will be executed asynchronously. More classical languages such as Java or C# use blocking pattern to block the function call until the function gets a response (though these languages nowadays also make a use of asynchronous processing). For efficiency reasons JavaScript does not do that, but will resume executing the code right away and return a Promise instead for method call that cannot be processed immediately which in turn either will get fulfilled at some time in future or get rejected (when something goes wrong). This is a good blog about the inner workings of JavaScript and the uniqueness of Event Loop, albeit a bit technical and focuses on browser environment, then in reality it is no different to Horizon JavaScript Runtime environment. In that blog asynchronous code completion is referred to be executed in callbacks. Callbacks are the lowest level concepts, and Promises are abstractions on top of them to make working with asynchronous code execution a bit more easy. Nowadays there is even another abstraction available on top of Promises that is called async/await that makes it possible to write seemingly more sequential code that allows to use more imperative coding style that a lot of people are more familiar with.

When it comes to Managed APIs, it is important to know that each function is processed asynchronously and will return a Promise. End users have 2 options when it comes to working with Promises, they can either work with Promises directly, or use async/await style on top of Promises. It is entirely up for them to decide which style they use, they can also choose to mix both styles.

As an example, let's first have a look at how the Promise style works. Following example fetches the content from google.com and prints it out:

export function run() {
    fetch('https://google.com') // Fetch contents of google.com
        .then(res => res.text()) // Ask the body to be returned in text format (also asynchronous call), note how Promises can be chained
        .then(body => console.log(body)); // Then print out the body
}
JS

And now let's have a look at the exact same code written in async/await style:

export async function run() {
    const response = await fetch('https://google.com'); // Fetch contents of google.com
    const body = await response.text(); // Ask the body to be returned in text format (also asynchronous call)
    console.log(body); // Then print out the body
}
JS

Abstractions

Fetch API

Horizon JavaScript Runtime by default provides Fetch API that serves as a general purpose HTTP client that can be used to make HTTP request to anywhere as long as the connection can be established and the target server is capable of talking over HTTP. In addition to standard Fetch API Horizon optionally accepts connection property that the end user can use to specify a managed connection. When the connection property is specified a user can then use relative URLs and does not have to provide authentication headers because that is automatically handled by Horizon platform. However, if a connection property is not specified user is expected to provide an absolute URL and necessary headers to establish a connection. Fetch API and its connection property is not considered part of the Managed API because this function exists and is always available for use in Horizon Runtime regardless of the products you are working with.

As an example, let's have a look at how to use Fetch API using Promise based approach, Here is an example how to use Fetch API using connect property:

// Promise style
export function run() {
    fetch('/rest/api/3/issue/ISSUE-1', {
        method: 'GET',
        connection: 'CONNECTION_ID' // Managed connection identifier
    }) // Fetch Jira issue
    .then(res => {
        if (res.ok) { // Check if response was OK
            return res.json(); // If it was then proceed by parsing the body into JSON, since .json() call returns a Promise it can be returned directly
        } else {
            return Promise.reject(new Error(`Unexpected response: ${res.status}`));  // If not then throw an error
        }
    })
    .then(issue => console.log(issue.fields.reporter.displayName)); // Print out reporter's display name
}
JS

To learn more how Promises can be chained and how Fetch API works please refer to official JavaScript documentation.

HTTP API

Functions in HTTP API abstraction can be found in {MANAGED_API_NAMESPACE}/http-api namespace. Functions in that namespace have to be imported directly, for example using getIssue function in Jira Cloud would have to be imported as following:

import { getIssue } from '@avst-hzn/jira-cloud-api-v3/http-api/issue';
JS

Please refer to product level documentation to learn about how functions are grouped in that namespace.

Functions in this namespace have following characteristics:

  • Functions are managed, requests and possible responses are typed, so you know what to expect.
  • Response object inherits much of the same properties as Fetch API response, such as headersok, status and statusText, but in difference to Fetch API body property returns all possible return types directly (non async).
  • Managed connection identifier has to be specified in function options.
  • Request body payloads are validated (can be turned off) pre-emptively. Note: only Jira Server right now

Use this abstraction level when you wish to:

  • Perform your own response validation. If something else goes wrong, such as connection fails then the error will be thrown.
  • Read response headers.

As an example, let's have a look at how reading Jira issue data works on this abstraction level, while also performing response validation. Since Jira makes use of HTTP status codes we can use either ok, status or statusText to perform the validation.

import { getIssue } from '@avst-hzn/jira-cloud-api-v3/http-api/issue';
import { Issue } from '@avst-hzn/jira-cloud-api-v3/types/issue';
import { AtlassianErrorResponse } from '@avst-hzn/jira-cloud-api-v3/common';
 
// Async/await style
export async function run() {
    // Get issue
    const response = await getIssue({
        connection: 'CONNECTION_ID', // Managed connection identifier
        issueIdOrKey: 'ISSUE-1' // Issue key
    });
    if (response.ok) { // Check if the response is OK (in 200-300 range)
        const issue = response.body as Issue.GetIssue.Response.OK; // If it is then cast the body to issue type
        console.log(issue.fields.reporter.displayName); // And print out the reporter's display name
    } else {
        const error = response.body as AtlassianErrorResponse; // If not then cast the body into error type
        console.error(`Error while reading issue: ${error.errorMessages.join(', ')}`); // And print out error messages
    }
}
JS

As another example, let's have a look at how reading a Slack channel works on this abstraction level, while also performing response validation. Since Slack does not make use of HTTP status codes (except for rate-limiting) but will always return ok property inside the body we have to explicitly check the body's ok property to assume what the response type is:

import { getChannelInfo } from 'adaptavist-slack-api/http-api/channel';
import { Channel } from 'adaptavist-slack-api/types/channel';
 
// Async/await style
export async function run() {
    // Get channel
    const response = await getChannelInfo({
        connection: 'CONNECTION _ID', // Managed connection identifier
        queryParams: {
            channel: 'CHANNEL_ID' // Channel ID
        }
    });
    if (response.ok) { // High level check to see if request is not rate limited or anything else didn't go terribly wrong
        if (response.body.ok) { // Check if the 'ok' param in response is true, then we can assume response type is OK
            const channel = response.body as Channel.Info.Response.OK; // If it is then cast the body to channel (OK) type
            console.log(channel.channel.name); // And print out the channel name
        } else {
            const error = response.body as Channel.Info.Response.Error; // If not then cast the body into error type
            console.error(`Error while reading channel info: ${error.error}`); // And print out the error message
        }
    } else if (response.status == 429) {
        // TODO: rate limited, add error handling logic
    } else {
        // TODO: something else went horribly wrong, add error handling logic
    }
}
JS

Managed API

Managed API is an abstraction on top of HTTP API which has the following characteristics:

  • Similar to HTTP API, request and response types are typed, so you know what to expect, and request body payloads are pre-emptively validated (can be turned off).
  • Functions no longer return generic response type, but exactly what you would expect, in case the request was successfully processed, otherwise an error will be thrown. Please read more about error handling in the error-handling section.
  • Response validation is automatically performed, as mentioned above, if something goes wrong an error will be automatically thrown.
  • Has an option to have additional error handling strategy specified to handle errors pre-emptively, such as performing automatic re-try logic on rate-limiting.
  • Still, managed connection identifier has to be specified on this level, as well.

Use this abstraction level if you wish to:

  • Not be bothered with manual response validations.
  • Use functions directly. If not, continue reading and use Fluent API abstraction instead.

Functions on this abstraction level can be found in {MANAGED_API_NAMESPACE}/managed-api namespace. Functions in that namespace have to be imported directly, for example using the getIssue function in Jira Cloud would have to be imported as following:

import { getIssue } from '@avst-hzn/jira-cloud-api-v3/managed-api/issue';
JS

Please refer to product-level documentation to learn about how functions are grouped in that namespace.

As an example, let's have a look at how reading Jira Issue data works on this abstraction level, without having to perform explicit response validation:

import { getIssue } from '@avst-hzn/jira-cloud-api-v3/managed-api/issue';
 
// Async/await style
export async function run() {
    try {
        // Get issue
        const issue = await getIssue({
            connection: 'CONNECTION_ID', // Managed connection identifier
            issueIdOrKey: 'ISSUE-1' // Issue key
        });
        console.log(issue.fields.reporter.displayName); // Print out reporter's display name
    } catch (e) { // Catching errors is explicit handled for example sake, if it wasn't caught then Runtime will catch it anyway
        console.error('Error while reading issue', e); // Something went wrong, lets print out the error
    }
}
JS

Fluent API

Fluent API is an abstraction built on top of Managed API and it works exactly the same, with one exception: connection property no longer has to be specified, nor can be. When working with Fluent API, the connection is specified once when creating a connection.

Fluent API exposes a single entry point where all functions can be accessed, and it aims to increase discoverability and remove the need to import each function separately, which can become tedious. As previously mentioned, functions behave exactly the same way as at the Managed API level but without the need to specify connection property separately because it is inherited.

To start working with Fluent API you must create a connection where you must specify managed connection property and, optionally, a set of options as following:

import { JiraCloudConnection } from '@avst-hzn/jira-cloud-api-v3';
 
const JiraCloud = JiraCloudConnection.connect('CONNECTION_ID');
JS

Once the connection has been created you can start using Fluent access pattern to drill down (dot walking) into the API to discover its capabilities. Inside the connection you will find groups and optionally sub-groups, inside groups you can find available functions:

A special All group is also present that contains all the functions in longer name format that are available, you can use All group together with code editor search capabilities to start looking for functions by typing for example 'MyJiraServer.All.remote' which should highlight all matching functions that contain 'remote' search term:

Functions in All group have a documentation snippet called 'Recommended usage' which highlights their original path in the grouping structure. While it is recommended, then it is not mandatory and it's perfectly fine to use functions in All group as they work exactly the same.

Please refer to product level documentation to learn about how functions are grouped.

As an example, let's have a look at how reading Jira Issue data works on this abstraction level, without having to import a function directly:

import { JiraCloudConnection } from '@avst-hzn/jira-cloud-api-v3';
 
// Async/await style
export async function run() {
    // Create the connection
    const JiraCloud = JiraCloudConnection.connect('CONNECTION_ID');
 
    // Get issue
    const issue = await JiraCloud.Issue.getIssue({
        issueIdOrKey: 'ISSUE-1'
    });
    console.log(issue.fields.reporter.displayName); // Print out reporter's display name
}
JS

Abstraction Layers Matrix

Parameters

Currently parameters are handled in two parts. If the parameter is originally part of the URL path or query parameters then parameters will be directly assignable in root level of the options. However, if parameters are part of the body, then they have to go into body property.

Sometimes official (vendor) documentation simply refers to as "parameters" without being explicitly clear which type of parameters they are, so if you cannot find these parameters in the root level be sure to check into body property to see if they are present there.

As an example, let's have a look at how parameters can be assigned:

await JiraCloud.Issue.updateIssue({
    issueIdOrKey: 'ISSUE-1', // Parameter that is part of the URL path
    body: {
        ...
    }
});
JS

Request payload validation

When the request contains a body the body will be pre-emptively validated against the schema, and if the validation fails an error called RequestValidationError will be thrown. If for some reason you need to skip the validation you can pass in skipValidation: true in options to skip the validation. Note: This feature may not end up in the final version, currently only Jira Server Managed API still uses request payload validation, for Jira Cloud and Slack this feature has been removed.

Error handling

When something goes wrong following error types can be thrown (Managed API level abstraction and above):

  • AnyError - Managed API won't actually throw just 'AnyError' error, it is there to denote that other error types besides what Managed API can throw can be thrown as by the user. In other words a user can throw 'any' error before the error handling can take place, that's why possible error types also include AnyError which loosely stands for 'any error that the end user can throw also'.
  • Unexpected Error - Denotes error type that is unexpected, this type of error usually is unrecoverable, for example connection failure, or JSON parsing failure because the server returned something that is not a JSON but when JSON was expected.
  • RequestValidationError - Thrown when request body validation fails (can be turned off).
  • HttpError - Thrown when the response status code is not in 200 range. Sometimes HttpError has the inner error of the actual error type that is returned, such as AtlassianErrorResponse in case of Jiras.

Sometimes additional error types may be thrown as well. In case of Slack for example, HttpError does not contain more defined type inside because Slack mostly does not make use of HTTP status code, and in case of Slack an additional error type can be thrown called SlackError which contains specific Slack error message.

Catching Errors

You can use instanceOf operator to catch specific error type.

Promise based example (Jira Cloud):

import { JiraCloudConnection } from '@avst-hzn/jira-cloud-api-v3';
import { UnexpectedError, HttpError } from '@avst-hzn/jira-cloud-api-v3/managed-api/common';
import { RequestValidationError } from '@avst-hzn/jira-cloud-api-v3/http-api/common';
 
export async function run() {
    const JiraCloud = JiraCloudConnection.connect('CONNECTION_ID');
 
    JiraCloud.Issue.getIssue({
        issueIdOrKey: 'ISSUE-1'
    })
    .then(issue => {
        // Do something with the issue
    })
    .catch(error => {
        if (error instanceof UnexpectedError) {
            // Do something with unexpected error
        } else if (error instanceof RequestValidationError) {
            // Do something with request validation error
        } else if (error instanceof HttpError) {
            console.error(error.response.body.errorMessages.join(', ')) // Print out Jira error messages that were sent back
        } else {
            // Anything else the user may have thrown, but if custom errors are not thrown then this case will never be necessary
        }
    });
}
JS

Similar example using async/await style, using Slack and narrowing down on Slack error types:

import { SlackConnection } from '@avst-hzn/slack-api';
import { UnexpectedError, HttpError, SlackError } from '@avst-hzn/slack-api/managed-api/common';
import { Channel } from '@avst-hzn/slack-api/types/channel';
import { RequestValidationError } from '@avst-hzn/slack-api/http-api/common';
 
export async function run() {
    const Slack = SlackConnection.connect('CONNECTION_ID');
 
    try {
        const channels = await Slack.Channel.getChannels();
        // Do something with channels
    } catch (error) {
        if (error instanceof UnexpectedError) {
            // Do something with unexpected error
        } else if (error instanceof RequestValidationError) {
            // Do something with request validation error
        } else if (error instanceof HttpError) {
            // Do something with HTTP error
            if (error.response.status == 429) { // Check if the request was rate limited by checking if the status code is 429
                // Do something when rate limiting occurs
            }
        } else if (error instanceof SlackError) {
            const errorType = (error as SlackError<Channel.List.Response.Error>).response.body.error // This casting is a bit clunky right now but will help narrow down exact possible error types
            if (errorType == 'invalid_auth') {
                // Do something when authentication fails
            } else {
                // Do something else
            }
        } else {
            // Anything else the user may have thrown, but if custom errors are not thrown then this case will never be necessary
        }
    }
}
JS

If you won't catch errors and let them bubble up then Runtime will catch them and print out the error in console.

While IntelliSense does not display it then common HTTP errors are thrown as sub-types of HttpError, which means instead of doing instanceOf and then checking for HTTP status code like shown in the Slack example above you can directly check for specific HTTP error sub-type as following:

try {
    // Do something
} catch (error) {
    if (error instanceof NotFoundError) { // NotFoundError is a sub-type of HttpError
        // Do something when 404 was returned
    }
}
JS

Please check the specific product level documentation to find out which common HTTP error sub-types are available because they differ based on how the actual product is making use of HTTP status codes.

Pre-emptive error handling

Sometimes it feels cumbersome to deal with the error it it already has been thrown, to avoid that errorStrategy property can be used to provide error handling strategy, if present, will prevent error from being thrown. Pre-emptive error handling can be handy in numerous situations, for example if you receive an error informing that the resource was not found (404) then you may want to provide error handling strategy to return a default value instead, or when your request was rate limited (429) you may simply want to retry the request without catching the error manually and calling the function again.

Managed API abstraction and above accept errorStrategy property, which has number of sub-properties that all are callback functions. These callback functions will be invoked when a specific error occurs. By default callback function takes in 2 parameters, the first is the error that occurred, and the second one is how many times the request has already been tried. Some products may have more specific error handlers available.

Callback functions expect some value to be returned, a value that is returned in a callback function will be the value that is returned from the actual Managed API function. As of now (might be subject to change) returning undefined is not allowed, the reasoning behind it is to force the code editor to let the user know that something needs to be returned, because if undefined was allowed to be returned the code editor won't be able to step in and warn the user because if a function does not return anything in JavaScript it will automatically return undefined. If you wish to return a nullish value you can explicitly return null instead.

Additional function calls can be returned as well:

  • retry(delay: number) - Tells the error handler to retry the request. Delay (optional) is measured in milliseconds and defaults to 0.
  • continuePropagation(skipErrorStrategyPropagation: boolean) - Asks the error handler to continue propagating the error, if skipErrorStrategyPropagation (optional, defaults to false) is set to true then error handler propagation is skipped and the actual error will be thrown. Continue reading about error propagation.

Universal error handlers that are present in all Managed APIs:

  • handleAnyError - Handles any errors.
  • handleUnexpectedError - Handles unexpected errors
  • handleRequestValidationError - Handles request validation errors.
  • handleHttpAnyError - Handles any HTTP errors.
  • handleHttp429Error - Handles rate limiting (429) HTTP errors.

Error handlers are hierarchical, meaning that if a more specific error handler exists then it will be invoked, but if a more specific one is not found a less specific one will be looked for. For example, if a server rate limits the request and sends back 429 response then the order of error handlers that will be looked for will be as following: handleHttp429Error, handleHttpAnyError, handleAnyError. The first one found will be invoked, but if none are found then TooManyRequestError which is a sub-type of HttpError will be thrown.

For illustrative purposes this is how the hierarchy looks like, top level error handlers are less specific and lower ones are more specific:

handleAnyError
handleHttpAnyErrorhandleUnexpectedErrorhandleRequestValidationError
handleHttp429Error

As an example, let's have a look how to return a default value 'Hello World!' when Jira issue is not found:

export async function run() {
    const issue = await JiraCloud.Issue.getIssue<string>({ // Optional type parameter denotes that 'string' type may be returned as well because the error handler can return a string
        issueIdOrKey: 'ISSUE-1',
        errorStrategy: {
            handleHttp404Error: () => 'Hello World!!!' // Value to return when server sends back 404 (Issue not found)
        }
    })
    // Do something with the issue
}
JS

As an example, let's have a look how to make use of retry and continuePropagation functions to implement retry logic when the request is rate limited (429). Error handler that will retry total of 3 times, initially wait for 1 second before retrying and after each retry will increase the delay 2 seconds:

import { JiraCloudConnection } from '@avst-hzn/jira-cloud-api-v3';
import { retry, continuePropagation } from '@avst-hzn/api-common';
 
export async function run() {
    const JiraCloud = JiraCloudConnection.connect('CONNECTION_ID');
 
    const issue = await JiraCloud.Issue.getIssue({
        issueIdOrKey: 'ISSUE-1',
        errorStrategy: {
            handleHttp429Error: (error, attempt) => attempt < 3 ? retry(1000 + ((attempt - 1) * 2000)) : continuePropagation() // Retry logic
        }
    })
    // Do something with the issue
}
JS

Obviously writing out retry logic like that is exhaustive so you can just use getRetryErrorHandler(total = 5, delay = 0, delayIncrease = 1000) convenience function that encapsulates the same logic, as following:

import { JiraCloudConnection } from '@avst-hzn/jira-cloud-api-v3';
import { getRetryErrorHandler } from '@avst-hzn/api-common';
 
export async function run() {
    const JiraCloud = JiraCloudConnection.connect('CONNECTION_ID');
 
    const issue = await JiraCloud.Issue.getIssue({
        issueIdOrKey: 'ISSUE-1',
        errorStrategy: {
            handleHttp429Error: getRetryErrorHandler(3, 1000, 2000) // Same retry logic as above
        }
    })
    // Do something with the issue
});
JS

We also expose ErrorStrategyBuilder for constructing errorStrategy object that contains useful implementations. Examples above could be expressed with ErrorStrategyBuilder as following:

const issue = await JiraCloud.Issue.getIssue<string>({
    issueIdOrKey: 'ISSUE-1',
    errorStrategy: JiraCloud.Builder.ErrorStrategy
        .http404Error(() => 'Hello World') // Return default value when Issue is not found (404)
        .retryOnRateLimiting(20) // Retry up to 20 times when request is being rate limited
        .build()
});
JS

You can also use a lambda function where the error strategy builder will be injected into, example above could be written in shorter as following (calling .build() is optional and can be omitted):

const issue = await JiraCloud.Issue.getIssue<string>({
    issueIdOrKey: 'ISSUE-1',
    errorStrategy: builder => builder
        .http404Error(() => 'Hello World') // Return default value when Issue is not found (404)
        .retryOnRateLimiting(20) // Retry up to 20 times when request is being rate limited
});
JS

If you are not using Fluent API or would like to user ErrorStrategyBuilder directly you can also achieve the same thing as following:

import { ErrorStrategyBuilder } from '@avst-hzn/jira-cloud-api-v3/builder/errorStrategy';
 
ErrorStrategyBuilder.create()
    .http404Error(() => 'Hello World')
    .retryOnRateLimiting(20)
    .build()
JS

You can also specify global error strategy for the connection which in turn gets applied to all functions called from from that connection, but can be optionally overwritten on function level. Specifying global error strategy works as following when creating a connection:

const JiraCloud = JiraCloudConnection.connect('CONNECTION_ID', {
    errorStrategy: ErrorStrategyBuilder.create()
        .http404Error(() => 'Hello World')
        .retryOnRateLimiting(20)
        .build()
});
JS

Or use a lambda function to specify global error strategy:

const JiraCloud = JiraCloudConnection.connect('CONNECTION_ID', {
    errorStrategy: builder => builder
        .http404Error(() => 'Hello World')
        .retryOnRateLimiting(20)
});
JS

By default Managed API connections come with sensible defaults for global error strategy, such as re-trying on rate limiting. If you do not wish to provide your own error strategy behaviour but wish to remove all default behaviours then pass null into the errorStrategy option as following:

const MyJiraServer = JiraServerConnection.connect('CONNECTION_ID', {
    errorStrategy: null
});
JS