# @keeex/sdk-helper


## General description

Shared code to build SDK libraries.

Features:

- Simplified raw HTTP calls
- Authentication
- Handling of input values transformations into either JSON or FormData
- Handling of file inputs
- Handling of generic pagination values
- Handling of URI arguments and query arguments
- Handling of file replies
- Grabing useful data from complex reply
- Progress callback
- Concurrent request limitation
- Separate request queues
- Input/output filtering

### Usage

Setting up a SDK using this library is done by providing settings describing how
the API is accessed, as well as individual descriptions for all API calls.
These individual descriptions are used to create functions that can be used to
call the API directly.

Short example:

```TypeScript
import SdkBase from "@keeex/sdk-helper/lib/sdk.js";
import {
  toAPI as dateToAPI,
  fromAPI as dateFromAPI,
} from "@keeex/sdk-helper/lib/filters/date";

const routeFilter = {
  toAPI: {lastKnownDate: dateToAPI},
  fromAPI: {newDate: dateFromAPI},
};

export default class Sdk extends SdkBase {
  public retrieveDate;

  constructor(config) {
    super(
      {baseUrl: config.uri}, // alternatively, provide {baseUrl, axios} if you want a custom axios
      {type: "autocookie"},
    );
    this.retrieveDate = this.directApiCall<{lastKnownDate: Date}, {newDate: Date}}({
      method: "post",
      route: "/api/v1/date/retrieve",
      authenticated: false,
      filters: routeFilter,
    });
  }
}

const sdk = new Sdk({uri: "https://invalid.invalid"});
const result = await sdk.retrieveDate({lastKnownDate: new Date()});
console.log("Reply:", result.newDate);
```

The above example create an SDK that exposes only one function called
`retrieveDate`.
This function takes as input an object with all the inputs (in this case there
is only one property: `lastKnownDate`).
It then returns the full reply from the server with one property: `newDate`.
The filter automatically convert a `Date` object into its ISO8601 string
representation before the call, and converts the reply back from an ISO8601
string representation to a `Date` object.

An alternative to `directApiCall()` named `createApiCall()` exists; it will return an object
exposing the raw reply body as well as the HTTP status code alongside the filtered data.

## Pagination

Instead of `directApiCall()` it is possible to call `paginatedApiCall()`.
In that case the input accepts extra `PaginationInput` fields, and the reply will follow the
`PaginatedResponse` interface.

## Authentication

This library handles two types of authentication: HTTP-header based and cookie
based.
The third argument of the `Sdk` class constructor defines the authorization
scheme to use.
If not provided, no authorization is handled.
If provided, routes that are defined with the `authenticated` property to true
requires the user to previously be authenticated.

To indicate to an `Sdk` instance that authentication completed, call the
`setAuthToken()` method with a suitable value.
`undefined` or any other false-y value indicates no authentication.

### Cookie-based authentication

This mode is basically a passthrough.
Cookies are handled by the browser.
To use this method of authentication, pass a config object with the single
property `type` set to `autocookie`.

### HTTP-header based authentication

This mode allows the caller to provide a value passed as an HTTP header.
In this case `setAuthToken())` expects a non-empty string argument to indicate
authentication.

To use this method, pass a config object with the following properties:

- `type`: must be `header`
- `headerName`: name of the header to use. Defaults to `Authorization`
- `valueTemplate`: a template for the value of the header; the word `%token%`
  will be replaced by the provided token. Defaults to `Bearer %token%`

## Defining API routes

To create a function that implements an API route, the `directApiCall()` method
must be called.
It takes a single parameter which define the behavior of the route.
It is an object with the following properties (all properties are optional
except for `route`):

- `axiosOptions`: custom axios options
- `authenticated`: true to restrict this call when non authenticated
- `binaryReply`: true if the reply is a binary buffer (`Uint8Array`)
- `bodyParams`: list of "input" properties to pass in the body
- `fileParams`: list of "input" properties that are expected to be files
- `filters`: filters to apply to input arguments and output values
- `hooks`: some hooks that are trigger before, after on succes or after on error
- `method`: HTTP method. Defaults to GET.
- `paginated`: true to handle special pagination "input" values (deprecated, see Pagination)
- `queryParams`: list of "input" properties to pass as query arguments
- `queue`: request queue to use. Defaults to a default queue.
- `route`: path to the API route relative to the base URL

The function returned by this call accepts a single parameter which is an
"input" object.
This "input" object's properties will then be parsed accordingly to the API
specification.

### URI arguments

It is possible to use some part of the "input" as URI arguments.
To do so, sections of the `route` must start with a `:` character.

For example, a route defined by `/api/v1/item/:id` will grab the `id` property
from the "input" object and put it in the URI before the request is performed.

### Query arguments

All properties names passed into `queryParams` will be appended to the route
using `?key=value` accordingly.

### Body arguments

All properties names passed into `bodyParams` will be put in either a JSON
object sent as the body for the request, or into a `multipart/form-data`,
depending on the presence of files or not in that request.

### File arguments

If `fileParams` is present, the body will be a `multipart/form-data` body and
treat the properties names in `fileParams` as files to be appended to it.

The file who's properties names are in `fileParams` are optional.

On most environment, a Blob can be passed for files in a request data.
Alternatively, on React-native, the following object can be passed as a valid value for a file
property:

```typescript
export interface FileDescriptionUri {
  /** File name sent. Defaults to "file" */
  name?: string;
  /** Mimetype. Defaults to "application/octet-stream" */
  type?: string;
  /** URI for the host. */
  uri: string;
}
```

### Hooks

There are 3 hooks available

```typescript
class RequestErrorProps extends {
  /** HTTP status code */
  public status: number;
  /** Actual reply from the request */
  public rawReply: unknown;
  /** Number of retries already tried (starts at 0) */
  public tries: number;
}

interface Hooks<InputDataType, ReplyDataType, ResultDataType> {
  /** Called before filtering input data */
  preSend?: (input: InputDataType) => Promise<InputDataType> | InputDataType;
  /** Called after successful (valid status) API call and after filtering reply */
  postSuccess?: (result: ResultDataType) => Promise<ResultDataType> | ResultDataType;
  /**
   * Called after invalid status reply
   *
   * Not called for network/filtering error.
   * Should throw to propagate an exception to the caller; can also return with a valid input to
   * retrigger the call
   */
  postError?: (error: RequestError, input: InputDataType) => Promise<InputDataType> | InputDataType;
}
```

#### preSend

This hook is called before the filters apply before doing the call. A typical use of this hook is to
validate data before doing the call. You can also modify the input data if needed.

### postSuccess

This hook is called after the call, after this filters are applied, allowing further modification of
the response

### postError

This hook is called after the call, before the filters are applied (they are only applied on valid
response). This hook let you fix the input data before retrying the call. A use case would be to
handle a 401 to re-authenticate.

### File replies

If a route returns a binary blob (a file) a special set `binaryReply` to `true`.
It will return an `Uint8Array` instead of a JSON object.

## Progress callback

Functions returned by `directApiCall()` accepts a second parameter: a function
that will be called regularly during the upload/download process.
This functions takes two arguments: the first is the progress percentage, the
second is a boolean set to true if we're in the upload phase.

## Queue control

The default setting of this library is to not allow more than two concurrent
requests on a single queue.
However, to allow for long operations to coexist with short requests, it is
possible to specify which "queue" is used by an API call.
By putting a queue name on the `queue` property of an API call description, it
can use a separate queue.

There is no restriction to the number of queues; keep in mind however that
running too many concurrent requests can results in dropped replies in some
circumstances.

If no queue name is provided a default queue is used.

## Data filtering

This library have a built-in option to filter/transform data before sending them
to the API, and when receiving a reply.

At its core, a filter is a function that takes a value of one type and returns
(using a promise) the converted value.
The `filters` property in API route definitions is an object that have two
properties:

- `toAPI`
- `fromAPI`

Each of these properties is an object whose keys represent the keys to filter
and values are filter functions.

A special case exists, for API calls that returns a single value and not an
object.
In that case, a special property indexed using the constant `filterRoot` can be set on the filters
list to be applied to the reply value itself and not on its properties.
If this property is present, no other properties will be handled.

### Built-in filters

A list of built-in filters is provided in the `lib/filters` directory:

- `binary`: conversion from/to buffers and hex/base64
- `date`: conversion from/to `Date` objects and ISO8601 string representations
- `fixed`: conversion from/to "fixed" decimal numbers, with fixed number of
  decimals
- `nullstring`: unify empty string and `null` values to/from `null`

### Utility filters

To be able to handle complex objects, special utility filters are available.
With them it is possible to filter data behind arrays and properties.

- `array`: apply a filter to all values of an array
- `props`: apply various filters to the properties of an object

## Migrating from v4.x to v5.x

### Breaking

#### Removed deprecated calls

- createAPICall: use createApiCall
- directAPICall: use directApiCall

#### Rename to all uppercase acronym to pascal case one

- all API are renamed to Api (ex. toAPI becomes toApi)
- all SDK are renamed to Sdk (ex. SDKReply becomes SdkReply)
- all URL are renamed to Url (ex. baseURL becomes baseUrl)
- all CB are renamed to Cb (ex. progressCB becomes progressCb)
- all AB are renamed to Ab (ex. fromApiAB becomes fromApiAb)
