All files / src HttpProvider.ts

92.85% Statements 39/42
90% Branches 9/10
100% Functions 7/7
94.28% Lines 33/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130  11x   11x 11x 11x   30x                                                                               11x 42x 38x 38x   11x 46x 46x                         11x                   49x     48x 48x 48x 46x 46x 46x                                 46x 46x   46x 46x 30x   16x 16x 12x 12x 1x   4x 4x                   2x      
/* eslint-disable @typescript-eslint/no-explicit-any */
import { HttpProviderConfigPaths, Provider, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { Logger } from 'ts-log';
import { fromSerializableObject, toSerializableObject } from '@cardano-sdk/util';
import axios, { AxiosAdapter, AxiosRequestConfig, AxiosRequestTransformer, AxiosResponseTransformer } from 'axios';
import packageJson from '../package.json';
 
const isEmptyResponse = (response: any) => response === '';
 
export interface HttpProviderConfig<T extends Provider> {
  /** The OpenApi version, which forms part of the URL scheme */
  apiVersion: string;
  /** Example: "http://localhost:3000" */
  baseUrl: string;
  /** A mapping between provider method names and url paths. Paths have to use /leadingSlash */
  paths: HttpProviderConfigPaths<T>;
  /** Additional request options passed to axios */
  axiosOptions?: AxiosRequestConfig;
  /**
   * Custom error handling: Either:
   * - interpret error as valid response by mapping it to response type and returning it
   * - map error to a new Error type by throwing it
   *
   * @param error response body parsed as JSON
   * @param method provider method name
   */
  mapError?: (error: unknown, method: keyof T) => unknown;
 
  /** This adapter that allows to you to modify the way Axios make requests. */
  adapter?: AxiosAdapter;
 
  /** Logger strategy. */
  logger: Logger;
 
  /** Slug used in the URL path */
  serviceSlug: string;
}
 
/** The subset of parameters from HttpProviderConfig that must be set by the client code. */
export type CreateHttpProviderConfig<T extends Provider> = Pick<
  HttpProviderConfig<T>,
  'baseUrl' | 'adapter' | 'logger'
> & {
  /** Override the OpenApi version */
  apiVersion?: string;
};
 
const transformResponse: AxiosResponseTransformer = (v) => {
  if (!v) return v;
  if (typeof v === 'string') v = JSON.parse(v);
  return fromSerializableObject(v, { errorTypes: [ProviderError] });
};
const transformRequest: AxiosRequestTransformer = (data) => {
  Iif (!data) return data;
  return JSON.stringify(toSerializableObject(data));
};
 
/**
 * Creates a HTTP client for specified provider type, following some conventions:
 * - All methods use POST requests
 * - Arguments are serialized using core toSerializableObject and sent as JSON `{args: unknown[]}` in request body
 * - Server is expected to use the following core utils:
 *   - fromSerializableObject after deserializing args
 *   - toSerializableObject before serializing response body
 *
 * @returns provider that fetches data over http
 */
export const createHttpProvider = <T extends Provider>({
  apiVersion,
  baseUrl,
  axiosOptions,
  mapError,
  paths,
  adapter,
  logger,
  serviceSlug
}: HttpProviderConfig<T>): T =>
  new Proxy<T>({} as T, {
    // eslint-disable-next-line sonarjs/cognitive-complexity
    get(_, prop) {
      const method = prop as keyof T;
      const urlPath = paths[method];
      if (!urlPath) return;
      return async (...args: any[]) => {
        try {
          const req: AxiosRequestConfig = {
            ...axiosOptions,
            adapter,
            baseURL: `${baseUrl.replace(/\/$/, '')}/v${apiVersion}/${serviceSlug}`,
            data: { ...args[0] },
            headers: {
              ...axiosOptions?.headers,
              'Content-Type': 'application/json',
              'Version-Api': JSON.stringify(apiVersion),
              'Version-Software': packageJson.version
            },
            method: 'post',
            responseType: 'json',
            transformRequest,
            transformResponse,
            url: urlPath
          };
          logger.debug(`Sending ${req.method} request to ${req.baseURL}${req.url} with data:`);
          logger.debug(req.data);
 
          const axiosInstance = axios.create(req);
          const response = (await axiosInstance.request(req)).data;
          return !isEmptyResponse(response) ? response : undefined;
        } catch (error) {
          if (axios.isAxiosError(error)) {
            if (error.response) {
              const typedError = error.response.data;
              if (mapError) return mapError(typedError, method);
              throw new ProviderError(ProviderFailure.Unknown, typedError);
            }
            if (error.request) {
              throw new ProviderError(ProviderFailure.ConnectionFailure, error, error.code);
            }
          }
          logger.error(error);
          throw new ProviderError(ProviderFailure.Unknown, error);
        }
      };
    },
 
    has(_, prop) {
      return prop in paths;
    }
  });