1
\$\begingroup\$

I'm writing a node.js module that is basically a wrapper for some API functions of third party company.

I tried to make it flexible and easy to fill with API functions from documentation and came up with this.

Module returns an instance of the API Client that can connect to API and stay connected (so it basically works like MongoCli, except it's not a db, but I find it easy to use that way).

/**
 * Creates NfieldClient object
 * @constructor NfieldClient
 */
function NfieldCliConstructor (defOptions) {

  var defaultRequestCliOptions = {
    baseUrl : 'https://api.nfieldmr.com/',
    headers : {
      'Content-Type' : 'application/json'
    }
  };

  extend(true, defaultRequestCliOptions, defOptions);

  /**
   * Returns a new instance of NfieldClient with new request parameters
   */
  this.defaults = function defaults (options) {
    if (typeof options !== 'object') throw new Error('`options` must be an object with request parameters');
    return new NfieldCliConstructor(options);
  };

  /**
   * Connects NfieldClient to API
   * Returns ConnectedInstance
   */
  this.connect = function connect (credentials, callback) {
    if (typeof credentials === 'undefined' || typeof credentials === 'function') throw new Error('not all required parameters provided: no `credentials`');
    if (!credentials.Domain || !credentials.Username || !credentials.Password) throw new Error('not all required parameters provided: no `Domain` or `Username` or `Password`');

    var token = {};
    var promise = SignIn(defaultRequestCliOptions, credentials).then(function (data) {
      if (data[0].statusCode !== 200) {
        throw new Error(`${data[0].statusCode}: ${data[0].body.Message}`);
      } else {
        token = {
          AuthenticationToken : data[0].body.AuthenticationToken,
          Timestamp : Date.now()
        };
        return new ConnectedInstance(defaultRequestCliOptions, token, credentials);
      }
    }).nodeify(callback);

    return promise;
  };

}

module.exports = {
  NfieldClient : new NfieldCliConstructor()
};

The .connect function from Client returns ConnectedInstance that contains all API functions and refreshes API token if needed:

/**
 * Wrapper function for all Nfield API requests that checks if API token is not outdated, and refreshes it otherwise
 */
function requestWithTokenCheck (defOptions, credentials, token, options, callback) {
  var returnedPromise;
  var chainedPromise;

  extend(true, options, defOptions);
  chainedPromise = request(options);

  if (Date.now() - token.Timestamp > tokenUpdateTime) {
    returnedPromise = SignIn(defOptions, credentials).then(function (data) {
      if (data[0].statusCode !== 200) {
        throw new Error(`${data[0].statusCode}: ${data[0].body.Message}`);
      } else {
        token.AuthenticationToken = data[0].body.AuthenticationToken;
        token.Timestamp = Date.now();
        return chainedPromise;
      }
    });
  } else {
    returnedPromise = chainedPromise;
  }

  returnedPromise.nodeify(callback);
  return returnedPromise;
}

/**
 * Returns Nfield survey status
 * 
 * {@link https://api.nfieldmr.com/help/api/get-v1-surveys-surveyid-fieldwork-status}
 */
function getSurveyStatus (defOptions, credentials, token, surveyId, callback) {
  var options = {
    method : 'GET',
    uri : `v1/Surveys/${surveyId}/Fieldwork/Status`,
    headers : {
      'Authorization' : `Basic ${token.AuthenticationToken}`
    }
  };

  return requestWithTokenCheck(defOptions, credentials, token, options, callback);
}

/**
 * Creates an instance of object, connected to Nfield API
 */
function ConnectedInstance (requestOptions, authToken, credentials) {
  this.__REQUEST_OPTIONS = requestOptions;
  this.__TOKEN = authToken;
  this.__CREDENTIALS = credentials;

  this.SurveyFieldwork = {
    status : getSurveyStatus.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN),
    start : startSurvey.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN),
    stop : stopSurvey.bind(this, this.__REQUEST_OPTIONS, this.__CREDENTIALS, this.__TOKEN)
  };
}

Full code could be found on GitHub.

There are several things that bothering me:

  1. Is it okay to use Promises like in the requestWithTokenCheck function? The tokens in the API are good only for 15 minutes, so I use this function as a request wrapper to renew token if it is too old, but maybe it is better to leave this 15 minutes sessions as-is and build this module for use in 'short sessions', never updating this token if it is too old?

  2. I like the use of .bind in ConnectedInstance with API functions that can be used outside the instance and just 'connected' to it with bind, but it looks pretty terrible when defined and maybe it is a bad practice in general to overuse .bind?

  3. I wanted to add a possibility to change default request options to use this modules under proxy or have an easy way to change API server if needed, but my solution with .defaults bothering me a little. Is it okay to return a new instance of object from this object constructor?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.