my-zxventures-axios-cache

1.0.4 • Public • Published

@zxventures/axios-cache (icp-pkg-axios-cache)

Table of Contents

Overview (or Why)

The goal of this library was to enable Axios along with a fast cache handling and to fix a few issues with the current library:

  • Requests with status 200 but with errors node present.
  • Lack of control over internal data management, for example, serialized errors.
  • High level of complexity for use, during the development process.
  • Simplify the clusters handling.

Quick start

Requirements

  • Redis server must be installed and the service must be enabled.
  • Supports Redis >= 2.6.12
  • Node.js >= 12.22.0

Install

This package is scoped and is private by default.

Therefore, to use this package, it is necessary to include a file named .npmrc at the root of the project where this package will be used with the following content:

//registry.npmjs.org/:_authToken=PUT-THE-AUTHENTICATION-TOKEN-HERE

NOTE: The authentication token should be requested from the infrastructure team.

Using npm:

npm install @zxventures/axios-cache

Usage

In this package there are some clients, helpers, validations and others. All of them can be customized, extended or used individually.

However, the simplest way is to use the axiosCache function. This function allows us to create a new instance of axios with some extra features to work with redis cache and custom validation of responses with status 200 but with the error node present, usually in GraphQL responses.

In the same way that axios works, it can be a general instance with some specific configuration. Likewise, it can override the instance configuration when using the request function or some shortcut functions like .get, .post, etc.

By default, caching is disabled, but it can be enabled by setting some properties in the configuration.

The axiosCache function receives as an optional parameter a configuration. This configuration has 2 optional properties. One of them is axiosConfig as its name indicates is the axios configuration (in a normal way) with 2 additional properties: validateResponse and cache. Another property is redisClientConfig and it is to set the custom redis client configuration.

const axiosCache = require('@zxventures/axios-cache');

const config = {
  axiosConfig: { /* ... */ },
  redisClientConfig: { /* ... */ }
};

const axiosInstance = axiosCache(config);

The custom redis client in this package uses the ioredis library to work with redis. Therefore, the complete configuration of redis is specified in the official documentation of the ioredis library. So then, the redisClientConfig configuration has the following schema and default values:

const axiosCache = require('@zxventures/axios-cache');

const redisClientConfig = {
  /*
   *  ioredis configuration to work with a standard redis server (no cluster)
   *  - https://github.com/luin/ioredis
   *  - https://luin.github.io/ioredis/classes/Redis.html
   *
   */
  redisConfig: {
    lazyConnect: true,
    connectTimeout: 10000,
    enableOfflineQueue: false,
  },

  /*
   *  ioredis configuration to work with the Cluster approach
   *  - https://github.com/luin/ioredis#cluster
   *  - https://luin.github.io/ioredis/classes/Cluster.html
   *
   */
  clusterConfig: {
    nodes: [],
    options: { },
  },

  /*
   *  Additionals props
   *
   */

  // `maxAgeInMs` defines the default cache lifetime in milliseconds.
  // - Type: Number
  // - Default value is `Number.NaN`.
  maxAgeInMs: Number.NaN,
};

const axiosInstance = axiosCache({ redisClientConfig });

To enable the cache, you only have to set the values of the properties in the cache property correctly.

const axiosCache = require('@zxventures/axios-cache');

const axiosConfig = {
  cache: {
    // `isActive` indicates whether or not to use the cache.
    // - Type: Boolean
    // - Default value is `false`.
    isActive: true, 

    // `maxAgeInMs` defines the cache lifetime in milliseconds.
    // this value must be greater than zero in order to be valid to use the cache.
    // - Type: Number
    // - Default value is `Number.NaN`.
    maxAgeInMs: 1000 * 30, // e.g: 30 seconds

    // `methodsToCache` to define which http methods will be valid to use the cache.
    // - Type: Array of strings
    // - Default value is `['get', 'post']`.
    methodsToCache: ['get', 'post'],

    // `configPathsToCreateKey` allows to define which configuration paths/properties
    // will be used to create the cache key.
    // If the value is not valid (e.g: empty array or a null/undefined) the cache key 
    // will be created using the full config.
    // - Type: Array of strings (no deep path/property)
    // - Default value is `['method', 'url', 'params', 'data']`.
    configPathsToCreateKey: ['method', 'url', 'params', 'data'],
  },
};

const axiosInstance = axiosCache({ axiosConfig });

Note that to enable and be able to use the cache, at least the following rules must be met:

  1. The value of config.cache.isActive must be true.
  2. The value of config.cache.maxAgeInMs greater than zero.
  3. One of the array values in config.cache.methodsToCache must match the config.method specified in an axios request.

Examples

Below are some useful examples that will help you understand how this library works.

- Example #1

This is an example of a simple get request using cache.

Note how the flow is logged (console.log) with some of the most relevant values in each step. Also, when axiosCache is used, the response schema has an additional property called isCachedData.

const axiosCache = require('@zxventures/axios-cache');

// general variables
const traceTime = "Example #1 - Time taken";

// setup the configs
const axiosConfig = {
  cache: {
    isActive: true, 
    maxAgeInMs: 1000 * 60, // 1 min.
  },
};

const redisClientConfig = {
  redisConfig: {
    host: process.env.AXIOS_CACHE_HOST,
    port: process.env.AXIOS_CACHE_PORT,
  },
};

// starts a timer that can be used to compute the duration of an operation
console.time(traceTime);

// create an axios instance using the configurations
const axiosInstance = axiosCache({ axiosConfig, redisClientConfig });

// make a get request
const response = await axiosInstance.get('https://httpstat.us/200');

// log the response
const { isCachedData, status, statusText, data } = response;
console.log('response', { isCachedData, status, statusText, data });

// stop the timer and print the result
console.timeEnd(traceTime);

/*  
 * Console output:
 * ———————————————————————
 *  • [RedisClient] connect() { statusBeforeTryingToConnect: 'wait', isConnected: false }
 *  • [RedisClient] get() {
 *    cachedDataFound: false,
 *    key: '39141ac79410dd0cb7948b0d83d398c69f3247c96a3219d1a19ebe53a5fdafe6'
 *  }
 *  • [adapter] request(): axiosConfig {
 *   method: 'get',
 *   url: 'https://httpstat.us/200',
 *   data: undefined,
 *   headers: { Accept: 'application/json...' },
 *   adapter: [Function (anonymous)],
 *   validateResponse: [Function: validateResponse],
 *   cache: {
 *     isActive: true,
 *     maxAgeInMs: 60000,
 *     methodsToCache: [ 'get', 'post' ],
 *     configPathsToCreateKey: [ 'method', 'url', 'params', 'data' ]
 *   }
 *   ...
 *  }
 *  • [RedisClient] set() {
 *    ms: 60000,
 *    key: '39141ac79410dd0cb7948b0d83d398c69f3247c96a3219d1a19ebe53a5fdafe6'
 *  }
 *  • [RedisClient] disconnect() { statusBeforeTryingToDisconnect: 'ready', isConnected: true }
 * response {
 *    isCachedData: false,
 *    status: 200,
 *    statusText: 'OK',
 *    data: { code: 200, description: 'OK' }
 *  }
 * Example #1 - Time taken: 797.941ms
 * 
 */

If the above example is run again, the output looks like this:

/*  
 * Console output:
 * ———————————————————————
 *  • [RedisClient] connect() { statusBeforeTryingToConnect: 'wait', isConnected: false }
 *  • [RedisClient] get() {
 *    cachedDataFound: true,
 *    key: '39141ac79410dd0cb7948b0d83d398c69f3247c96a3219d1a19ebe53a5fdafe6'
 *  }
 *  • [RedisClient] disconnect() { statusBeforeTryingToDisconnect: 'ready', isConnected: true }
 * response {
 *    isCachedData: true,
 *    status: 200,
 *    statusText: 'OK',
 *    data: { code: 200, description: 'OK' }
 *  }
 * Example #1 - Time taken: 49.522ms
 * 
 */

Note that the request has not been made and the set method is not called (RedisClient). Also, now the value of the isCachedData property is true and the time spent is much less.

- Example #2

This is a common example of a GraphQl endpoint request. In this case the GraphQl endpoint will response with a status 200 but with errors node present.

const axiosCache = require('@zxventures/axios-cache');

// setup the configs
const axiosConfig = {
  cache: {
    isActive: true, 
    maxAgeInMs: 1000 * 60, // 1 min.
  },
};

const redisClientConfig = {
  redisConfig: {
    host: process.env.AXIOS_CACHE_HOST,
    port: process.env.AXIOS_CACHE_PORT,
  },
};

// create an axios instance using the configurations
const axiosInstance = axiosCache({ axiosConfig, redisClientConfig });

// create a request data
const sellerId = 'modeloramamxcentro';
const appToken = 'eyJ0eXAiOiJqd3QiLCJhbGciOiJ...';
const authorization = 'eyJraWQiOiJSd2tEbTZkRjBtQksrd...';

const requestData =  {
  method: 'POST',
  url: 'https://dev-api.modelorama.online/graphql/gateway',
  headers: {
    appToken,
    authorization,
  },
  data: {
    variables: {
      input: {
        sellerId,
      },
    },
    query: `
      query minimumOrderValueQuery($input: MinimumOrderValueInput) {
        minimumOrderValue(input: $input)
      }
    `,
  },
};

try {
  // make a request
  const response = await axiosInstance.request(requestData);

  // log the response
  const { isCachedData, status, statusText, data } = response;
  console.log('response', { isCachedData, status, statusText, data });
} catch (error) {
  // log the error
  const { message, code, name, innerError } = error;
  console.error('error', { message, code, name, innerError });
}

/*  
 * Console output:
 * ———————————————————————
 *  • [RedisClient] connect() { statusBeforeTryingToConnect: 'wait', isConnected: false }
 *  • [RedisClient] get() {
 *    cachedDataFound: false,
 *    key: '510bff30f5ac9d7d46104bddde88d3fe612769ceead537d56979bf637aee1a91'
 *  }
 *  • [adapter] request(): axiosConfig {
 *   method: 'post',
 *   url: 'https://dev-api.modelorama.online/graphql/gateway',
 *   data: '{"variables":{"input":{"sellerId":"modeloramamxcentro"}},"query":"..."}',
 *   headers: { 
 *     Accept: 'application/json...',
 *     'Content-Type': 'application/json',
 *     appToken: 'eyJ0eXAiOiJqd3QiLCJhbGciOiJ...',
 *     authorization: 'eyJraWQiOiJSd2tEbTZkRjBtQksrd...',
 *   },
 *   adapter: [Function (anonymous)],
 *   validateResponse: [Function: validateResponse],
 *   cache: {
 *     isActive: true,
 *     maxAgeInMs: 60000,
 *     methodsToCache: [ 'get', 'post' ],
 *     configPathsToCreateKey: [ 'method', 'url', 'params', 'data' ]
 *   }
 *   ...
 *  }
 *  • [RedisClient] disconnect() { statusBeforeTryingToDisconnect: 'ready', isConnected: true }
 * error {
 *   message: 'Error trying to get the minimum order value',
 *   code: 'IOP-CHK-3001',
 *   name: 'AxiosCacheError',
 *   innerError: [AxiosError: Error trying to get the minimum order value] {
 *     ...
 *   },
 *  }
 * 
 */

Note that the request fails with an error of type AxiosCacheError which inherits from CustomBaseError (base error type from @zxventures/utils library). This is the error type used by this library to wrap the original error (AxiosError) and has 3 main properties: message, code and innerError.

Thanks to the extra property called validateResponse the new flow (custom adapter) checks the response object with status 200 but with internal error nodes. Thus, it converts the response into an error using the information obtained from the internal error nodes.

To customize the validateResponse function it is only necessary to override the original custom function This function receives a single argument called response and no results are expected from it but you can alter the response object or throw an error.

It is important to note that at this point in the axios flow there are 2 things to keep in mind:

  • the 'response.data' must be a String (serialized data).
  • transform responses haven't been executed yet.
const axiosCache = require('@zxventures/axios-cache');

// setup the axios config
const axiosConfig = {
  // `cache` defines how to work with axios-cache
  // - Type: Object
  cache: { /* ... */ },

  // `validateResponse` defines a function that receives a single argument called `response`
  // and no results are expected from it.
  // - Type: Function (void)
  // - Default value is custom validation to check the response object with 
  //   status 200 but with internal error nodes.
  validateResponse: (response) => { /* some custom validation */ }
};

// create an axios instance using the configuration
const axiosInstance = axiosCache({ axiosConfig });

// ...

- Example #3

This is an example where a base cache configuration is used in a global instance of axios. Then, when a request is made, the cache configuration is overwritten. It is useful when a specific use case requires some more specific configuration, for example: changing the lifetime of a cache or enable/disable the cache.

const axiosCache = require('@zxventures/axios-cache');

// setup the configs
const axiosConfig = {
  cache: {
    isActive: true, 
    maxAgeInMs: 1000 * 30, // 30 sec
  },
};

const redisClientConfig = { /* ... */ };

/*
 * `Client` with two particular methods (specific use cases)
 *  - getProductList(): change the lifetime of a cache
 *  - getAddressById(id): disable the cache
 *
 */
class Client {
  constructor() {
    this.axiosInstance = axiosCache({ axiosConfig, redisClientConfig });
  }

  async getProductList() {
    const config = {
      cache: { 
        // change the value of `maxAgeInMs` to 5min instead of 30sec.
        maxAgeInMs: (1000 * 60) * 5 
      },
      method: 'POST',
      headers: { /* appToken, authorization and others */ }, 
      data: { /* some request data */ },
    };

    const response = await this.axiosInstance.request(config);
    return response;
  };

  async getAddressById(id) {
    const config = {
      cache: { 
        // disable cache
        isActive: false,
      },
      method: 'POST',
      headers: { /* appToken, authorization and others */ }, 
      data: { /* some request data */ },
    };

    const response = await this.axiosInstance.request(config);
    return response;
  };

  // other methods using the standard instance of `axiosInstance`.
  // ...
}

module.exports = Client;

- Example #4

This is an example using the redis cluster. To work with the redis cluster it is only necessary to use the clusterConfig property instead of the redisConfig property and follow the instructions/guidelines of the official documentation of the ioredis library to work with cluster.

const axiosCache = require('@zxventures/axios-cache');

// setup the configs
const axiosConfig = {
  cache: {
    isActive: true, 
    maxAgeInMs: 1000 * 60, // 1 min.
  },
};

const redisClientConfig = {
  // - https://github.com/luin/ioredis#cluster
  clusterConfig: {
    nodes: [{
      port: 6380,
      host: "127.0.0.1",
    },
    {
      port: 6381,
      host: "127.0.0.1",
    }],
    options: { /* ... */ },
  },
};

// create an axios instance using the configurations
const axiosInstance = axiosCache({ axiosConfig, redisClientConfig });

// Now the `axiosInstance` is working with redis cluster.
// ...

Run unit test and code coverage

ADVICE: It is recommended to review and run the unit tests to better understand each of the utilities provided by this package.

Run

npm test

To run code coverage use

npm run coverage

Package Dependencies

In this section, include the packages used as dependencies to build this package.

Some examples can be:

Contributors

Package Sidebar

Install

npm i my-zxventures-axios-cache

Weekly Downloads

0

Version

1.0.4

License

ISC

Unpacked Size

87.2 kB

Total Files

34

Last publish

Collaborators

  • al3x-onetree