Skip to content

Instantly share code, notes, and snippets.

@swinz
Last active January 23, 2024 04:30
Show Gist options
  • Save swinz/b7dee5a201ee46497f3d5a8a856fb14b to your computer and use it in GitHub Desktop.
Save swinz/b7dee5a201ee46497f3d5a8a856fb14b to your computer and use it in GitHub Desktop.
Example of a wrapped API Manager via a custom Axios instance

Example Usage

Initialization

const TinyurlApiManager = require('./src/tinyurl_api_manager');
let tMgr = new TinyurlApiManager({apikey: '<YourApiKeyHere>'});

Create Tinyurl

Create a tinyurl as follows:

  let createRes = await tMgr.createUrl({
    url: "https://en.wikipedia.org/wiki/John_Wick_(film)", 
    correlationId: "3ac6d341-f190-48a3-86bb-7bd90b897fef"
  });

  // createRes set to: 
  // {
  //   alias: 'kepkwwmm',
  //   archived: false,
  //   url: 'https://en.wikipedia.org/wiki/John_Wick_(film)',
  //   tiny_url: 'http://tinyurl.com/kepkwwmm'
  // }

Get Existing Tinyurl

Retrieve the tinyurl you just created as follows:

let getRes = await tMgr.getUrl({alias: createRes.alias});

// getRes set to: 
// {
//   alias: 'kepkwwmm',
//   archived: false,
//   url: 'https://en.wikipedia.org/wiki/John_Wick_(film)',
//   hits: 0,
//   tiny_url: 'http://tinyurl.com/kepkwwmm'
// }

Update Existing Tinyurl

Update the tinyurl you created by providing a new target url for the alias:

let updateRes = await tMgr.updateUrl({
  alias: createRes.alias, 
  url: "https://en.wikipedia.org/wiki/John_Wick"
});

// updateRes set to: 
// { url: 'https://en.wikipedia.org/wiki/John_Wick', alias: 'kepkwwmm' }

Get List of Available Tinyurls

Fetch the list of tinyurls in your account as follows:

let available = await tMgr.getAvailable();

// available set to:
// [
//   {
//     alias: 'kepkwwmm',
//     archived: false,
//     tiny_url: 'http://tinyurl.com/kepkwwmm'
//   }
// ]

Archive a Tinyurl

Archive the tinyurl you created as follows:

let archiveRes = await tMgr.archiveUrl({alias: createRes.alias});

// archiveRes set to: 
// {
//   alias: 'kepkwwmm',
//   archived: true,
//   tiny_url: 'http://tinyurl.com/kepkwwmm'
// }

Get List of Archived Tinyurls

Fetch the list of tinyurls in your account that you have archived as follows:

let archived = await tMgr.getArchived();

// archived set to:
// [
//   {
//     alias: 'kepkwwmm',
//     archived: true,
//     tiny_url: 'http://tinyurl.com/kepkwwmm'
//   }
// ]

Documentation

TinyurlApiManager

Wraps the tinyurl webservice into a concise and easy to use manager object

Kind: global class

tinyurlApiManager.getUrl(args) ⇒ Object

Fetch a tinyurl with an alias

Kind: instance method of TinyurlApiManager
Returns: Object - {tiny_url, ...}

Param Type Default Description
args Object
args.alias string alias identifier of tinyurl, Required.
args.enrich boolean false should the response be enriched with custom calculated values? Optional
args.correlationId string the correlation id under which this should be logged and x-correlation-id header, Optional
If missing, a random value is generated.

tinyurlApiManager.createUrl(args) ⇒ Object

Create a new tinyurl alias

Kind: instance method of TinyurlApiManager
Returns: Object - {tiny_url, ...}

Param Type Default Description
args Object
args.url string target url, Required.
args.enrich boolean false should the response be enriched with custom calculated values? Optional
args.correlationId string the correlation id under which this should be logged and x-correlation-id header, Optional
If missing, a random value is generated.

tinyurlApiManager.updateUrl(args) ⇒ Object

Update the target url of an existing tinyurl alias

Kind: instance method of TinyurlApiManager
Returns: Object - {url, alias}

Param Type Description
args Object
args.alias string alias identifier of tinyurl, Required.
args.url string new target url, Required.
args.correlationId string the correlation id under which this should be logged and x-correlation-id header, Optional
If missing, a random value is generated.

tinyurlApiManager.archiveUrl(args) ⇒ Object

Archive an existing tinyurl

Kind: instance method of TinyurlApiManager
Returns: Object - {tiny_url, alias, archived}

Param Type Description
args Object
args.alias string alias identifier of tinyurl, Required.
args.correlationId string the correlation id under which this should be logged and x-correlation-id header, Optional
If missing, a random value is generated.

tinyurlApiManager.getAvailable(args) ⇒ Array.<Object>

Retrieve a list of available tinyurls

Kind: instance method of TinyurlApiManager
Returns: Array.<Object> - {tiny_url, ...}

Param Type Description
args Object
args.correlationId string the correlation id under which this should be logged and x-correlation-id header, Optional
If missing, a random value is generated.

tinyurlApiManager.getArchived(args) ⇒ Array.<Object>

Retrieve a list of archived tinyurls

Kind: instance method of TinyurlApiManager
Returns: Array.<Object> - {tiny_url, ...}

Param Type Description
args Object
args.correlationId string the correlation id under which this should be logged and x-correlation-id header, Optional
If missing, a random value is generated.
const axios = require('axios');
const uuid = require('uuid');
const R = require('ramda');
const winston = require('winston');
const logger = winston.createLogger({
level: 'debug',
transports: [new winston.transports.Console()]
}).child({service: 'TinyurlApiManager'});
const MAX_CONTENT_LEN = 4096;
/**
* Wraps the tinyurl webservice into a concise and easy to use manager object
*/
class TinyurlApiManager {
constructor(opts) {
const {baseurl, domain, apikey} = opts;
this._apikey = apikey;
this._domain = domain || "tinyurl.com";
this._axios = axios.create({
validateStatus,
baseURL: (baseurl || "https://api.tinyurl.com/"),
maxContentLength: MAX_CONTENT_LEN,
headers: {
common: {
['Accept']: 'application/json',
['Content-Type']: 'application/json'
}
}
});
// API key is added to each request. It is static, so we can set here.
// If it were dynamic, we would login, retrieve+cache token in a request interceptor
this._axios.defaults.headers.common['Authorization'] = `Bearer ${this._apikey}`;
logger.info("initialized TinyurlApiManager", {baseurl: this._axios.defaults.baseURL, domain: this._domain});
// Setup Axios Request Interceptors -------------------
// Request Interceptor 1 of 1: Add dynamic headers to all requests
this._axios.interceptors.request.use(async config => {
let correlationId = config.headers['x-correlation-id'];
if (correlationId) {
logger.debug("CorrelationId request interceptor: correlationId header already set", { correlationId });
} else {
// correlation id: default to a random value unless it is already set
config.headers = Object.assign(config.headers, {
['x-correlation-id']: uuid.v4()
});
logger.debug("CorrelationId request interceptor: Dynamic headers were added to axios config", {
url: config.url,
headers: filterObject(config.headers, ['x-correlation-id'])
});
}
return config;
});
// Setup Axios Response Interceptors -------------------
// Response Interceptor 1 of 2: Enrich data when appropriate and the 'enrich' param passed
this._axios.interceptors.response.use(
response => {
// if the response payload contains an alias value AND the 'enrich' param is true, add the custom score data to the response
let resAlias = R.path(['data', 'data', 'alias'], response);
let paramEnrich = R.path(['config', 'params', 'enrich'], response);
let doEnrich = !!(resAlias && paramEnrich);
logger.debug("enrichment evaluation", {resAlias, paramEnrich, doEnrich});
if (doEnrich) {
response.data.data.enriched_custom_score = calculateCustomScore(resAlias);
}
logger.debug("post enrichment", {data:response.data});
return response;
});
// Response Interceptor 2 of 2: Uniform Logging and Error normalization:
// Failed calls return an Error object that contains a custom 'axiosError' attribute.
this._axios.interceptors.response.use(
response => {
logger.info("axios call success.", {baseurl: response.config.baseURL, url: response.config.url, status: response.status, statusText: response.statusText});
logger.debug("axios call success payload", {data:response.data});
return response;
},
async error => {
const axiosErrObj = {
url: R.pathOr('-', ['config', 'url'], error),
method: R.pathOr('-', ['config', 'method'], error),
errmsg: error.message,
errstack: error.stack,
errcode: error.code,
response_status: R.pathOr('-', ['response', 'status'], error),
response_body: R.pathOr('-', ['response', 'data'], error)
};
logger.warn("Failed axios call", {error, axiosErrObj});
let e = new Error(`axios error: ${axiosErrObj.errmsg}`);
e.axiosError = axiosErrObj;
return Promise.reject(e);
}
);
}
// ==================================
// Begin: Public interface
/**
* Fetch a tinyurl with an alias
* @param {Object} args
* @param {string} args.alias - alias identifier of tinyurl, <b>Required</b>.
* @param {boolean} args.enrich=false - should the response be enriched with custom calculated values? <b>Optional</b>
* @param {string} args.correlationId - the correlation id under which this should be logged and x-correlation-id header, <b>Optional</b><br/> If missing, a random value is generated.
* @returns {Object} {tiny_url, ...}
*/
async getUrl(args) {
const params = filterObject(args, ['alias', 'enrich']);
const opts = filterObject(args, ['correlationId']);
const path = `/alias/${this._domain}/${params.alias}`
validateTruthy(params.alias, "getUrl: missing required param: alias");
const response = await this._execAxios( {...opts, path, params});
const payload = R.path(['data', 'data'], response);
validateTruthy(payload, `getUrl: received empty response from api for alias=${params.alias}`);
return filterObject(payload, ['alias', 'archived', 'url', 'hits', 'tiny_url', 'enriched_custom_score']);
}
/**
* Create a new tinyurl alias
* @param {Object} args
* @param {string} args.url - target url, <b>Required</b>.
* @param {boolean} args.enrich=false - should the response be enriched with custom calculated values? <b>Optional</b>
* @param {string} args.correlationId - the correlation id under which this should be logged and x-correlation-id header, <b>Optional</b><br/> If missing, a random value is generated.
* @returns {Object} {tiny_url, ...}
*/
async createUrl(args) {
const params = filterObject(args, ['enrich']);
const opts = filterObject(args, ['correlationId']);
const body = filterObject(args, ['url']);
const path = '/create';
validateTruthy(body.url, "createUrl: missing required arg: url");
const response = await this._execAxios( {...opts, method: "post", path, params, data: {...body, domain: this._domain}});
const payload = R.path(['data', 'data'], response);
validateTruthy(payload, `createUrl: received empty response from api for url=${body.url}`);
return filterObject(payload, ['alias', 'archived', 'url', 'hits', 'tiny_url', 'enriched_custom_score']);
}
/**
* Update the target url of an existing tinyurl alias
* @param {Object} args
* @param {string} args.alias - alias identifier of tinyurl, <b>Required</b>.
* @param {string} args.url - new target url, Required.
* @param {string} args.correlationId - the correlation id under which this should be logged and x-correlation-id header, <b>Optional</b><br/> If missing, a random value is generated.
* @returns {Object} {url, alias}
*/
async updateUrl(args) {
const opts = filterObject(args, ['correlationId']);
const body = filterObject(args, ['url', 'alias']);
const path = '/change';
validateTruthy(R.path(['url'], body), "updateUrl: missing required arg: url");
validateTruthy(R.path(['alias'], body), "updateUrl: missing required arg: alias");
const response = await this._execAxios( {...opts, method: "patch", path, data: {...body, domain: this._domain}});
const payload = R.path(['data', 'data'], response);
validateTruthy(payload, `updateUrl: received empty response from api for alias=${body.alias}`);
// api response is limited
let resp_body = filterObject(payload, ['url']);
return {...resp_body, alias: body.alias};
}
/**
* Archive an existing tinyurl
* @param {Object} args
* @param {string} args.alias - alias identifier of tinyurl, <b>Required</b>.
* @param {string} args.correlationId - the correlation id under which this should be logged and x-correlation-id header, <b>Optional</b><br/> If missing, a random value is generated.
* @returns {Object} {tiny_url, alias, archived}
*/
async archiveUrl(args) {
const opts = filterObject(args, ['correlationId']);
const body = filterObject(args, ['alias']);
const path = '/archive';
validateTruthy(R.path(['alias'], body), "archiveUrl: missing required arg: alias");
const response = await this._execAxios( {...opts, method: "patch", path, data: {...body, domain: this._domain}});
const payload = R.path(['data', 'data'], response);
validateTruthy(payload, `archiveUrl: received empty response from api for alias=${body.alias}`);
return filterObject(payload, ['alias', 'archived', 'tiny_url']);
}
/**
* Retrieve a list of available tinyurls
* @param {Object} args
* @param {string} args.correlationId - the correlation id under which this should be logged and x-correlation-id header, <b>Optional</b><br/> If missing, a random value is generated.
* @returns {Object} {tiny_url, ...}
*/
async getAvailable(args) {
return await this._getList('/urls/available', args);
}
/**
* Retrieve a list of archived tinyurls
* @param {Object} args
* @param {string} args.correlationId - the correlation id under which this should be logged and x-correlation-id header, <b>Optional</b><br/> If missing, a random value is generated.
* @returns {Object} {tiny_url, ...}
*/
async getArchived(args) {
return await this._getList('/urls/archived', args);
}
// End: Public interface
// ==================================
// Begin: Private member helpers
// shared axios method exec function
async _execAxios(inOpts) {
// the allowed opts:
const { method, maxContentLength, path, params, data, correlationId } = {...inOpts};
// path must be present in opts
validateTruthy(path, "path must be a specified option in opts for _execAxios call");
const url = path;
const headers = {'x-correlation-id': correlationId};
const config = { method, url, headers, maxContentLength, params, data } ;
logger.debug("_execAxios: Executing an axios call", {config});
const res = await this._axios.request(config);
const retval = {status: res.status, statusText: res.statusText, data: res.data};
logger.debug("_execAxios: Response from axios call", retval);
return retval;
}
async _getList(path, args) {
const opts = filterObject(args, ['correlationId']);
const response = await this._execAxios( {...opts, path});
const payload = R.path(['data', 'data'], response);
validateIsArray(payload, `getList: received non-array response from api at: ${path}`);
return payload.map(e=>filterObject(e, ['alias', 'archived', 'tiny_url']));
}
// End: Private member helpers
}
module.exports = TinyurlApiManager;
// ==================================
// various private helper functions
// define success status codes
function validateStatus(status) {
return status >= 200 && status < 300;
}
function filterObject(obj, allowedAttributeNames) {
return R.pick(allowedAttributeNames || [], obj || {} );
}
function calculateCustomScore(val) {
// this is just an example to show response data enrichment: - calculate a value between 0-100 for val based on characters in val
// irl this would be more meaningful, but it's just a demo
return (""+(val || ""))
.split("")
.map(x=>x.charCodeAt(0)).reduce((acc, val) => acc+val, 0)
% 101;
}
function validateTruthy(val, msg) {
if (!val) {
throw new Error(msg);
}
}
function validateIsArray(val, msg) {
if (!Array.isArray(val)) {
throw new Error(msg);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment