index.js

/**
 * @module epochta-client
 */

const helpers = require('./helpers');
const errors = require('./errors');
const APIMethodsInterface = require('./api-methods');

/**
 * Simplified HTTP request client
 * @external
 * @see {@link https://github.com/request/request Request module}
 */
const request = require('request');

/**
 * @typedef {Object} Options
 * @property {boolean} [testMode=false] - if ```true``` - performs requests in API test mode and allows to use custom methods
 * @property {Object} [request] - HTTP request parameters
 * @property {string} [request.method=POST] - HTTP request method for API requests
 * @property {string} [request.baseUrl=http://api.myatompark.com/sms/3.0/] - Base url for API requests
 * @property {string} [request.apiVersion=3.0] - API version
 * @property {number} [request.correctResponseStatusCode=200]
 * @private
 */

/**
 * Options container
 * @type {module:epochta-client~Options}
 * @private
 */
const config = {
  testMode: false,
  request: {
    method: 'POST',
    baseUrl: 'http://api.myatompark.com/sms/3.0/',
    apiVersion: '3.0',
    correctResponseStatusCode: 200,
  },
};

/**
 * API Client instance
 * @type {module:epochta-client.APIClient}
 * @private
 */
let apiClientInstance = null;

/**
 * API keys provided by ePochta SMS service
 * @typedef {Object} APIKeys
 * @property {string} publicKey - public API key
 * @property {string} privateKey - private API key
 */

/**
 * API keys container
 * @type {module:epochta-client~APIKeys}
 * @private
 */
const apiKeys = {
  publicKey: '',
  privateKey: '',
};

/**
 * @static
 * @class
 * @classdesc Singleton constructor of API Client creates new instance or returns existing if has one.
 * Implements all methods of {@link module:epochta-client~APIMethodsInterface APIMethodsInterface}
 * @implements {module:epochta-client~APIMethodsInterface}
 * @param {module:epochta-client~APIKeys} keys - API keys provided by ePochta SMS service
 * @param {boolean} [isTestMode=false] - if 'true' adds argument test=1 to each API request and allows custom request methods
 */
const APIClient = function(keys, isTestMode) {
  if (!apiClientInstance) {
    // checks for provided keys
    if (!keys ||
        !keys.hasOwnProperty('publicKey') ||
        !keys.hasOwnProperty('privateKey')) {
      throw new Error('provide publicKey and privateKey');
    }
    if (isTestMode) {
      config.testMode = true;
    }
    Object.assign(apiKeys, keys);
    Object.freeze(apiKeys);
    // implements methods of APIMethodsInterface through proxy
    apiClientInstance = new Proxy(APIMethodsInterface, clientHandler);
  }
  return apiClientInstance;
};

/**
 * Placeholder object which contains traps
 * @type {object}
 * @private
 */
const clientHandler = {};

/**
 * Method that provide property access on getting property or method
 * @param {object} target Object which the proxy virtualizes
 * @param {string} methodName The name of the property/method to get.
 * @return {Proxy} Proxy for virtualization all method calls
 * @private
 */
clientHandler.get = function getTrap(target, methodName) {
  let method;
  // checks availability of called method in API if not in test mode
  if (!config.testMode) {
    if (!target[methodName] && typeof target[methodName] !== 'function') {
      throw new Error(`${methodName} method doesn't exist in API`);
    }
    method = target[methodName];
  } else {
    // empty anonymous function with name = methodName
    method = new Proxy(new Function(), {
      get: function() {
        return methodName;
      },
    });
  }
  return new Proxy(method, clientMethodsHandler);
};

/**
 * Placeholder object which contains traps
 * @type {object}
 * @private
 */
const clientMethodsHandler = {};

/**
 * Trap for APIMethodsInterface methods call
 * @param {function} targetMethod Method of APIMethodsInterface which has been called
 * @param {object} thisArg The this argument for the call
 * @param {Array} args The list of arguments for the call.
 * @return {Promise}
 * @private
 */
clientMethodsHandler.apply = function applyTrap(targetMethod, thisArg, args) {
  const apiRequestArguments = args[0];
  // checks for provided arguments
  if(!apiRequestArguments || typeof apiRequestArguments != 'object') {
    throw new Error('No arguments provided');
  }
  // checks for availability of required arguments with default APIMethodsInterface method
  if (!config.testMode && !targetMethod(apiRequestArguments)) {
    throw new Error('Some required arguments are missing');
  }
  // checks for test mode
  if (config.testMode) {
    apiRequestArguments.test = 1;
  }
  // API request arguments assembly
  apiRequestArguments.key = apiKeys.publicKey;
  const tempArgs = {};
  Object.assign(tempArgs, apiRequestArguments);
  tempArgs.version = config.request.apiVersion;
  tempArgs.action = targetMethod.name;
  apiRequestArguments.sum = helpers.checksum(tempArgs, apiKeys.privateKey);
  // API HTTP request
  return new Promise(function(resolve, reject) {
    request({
      method: config.request.method,
      baseUrl: config.request.baseUrl,
      uri: targetMethod.name,
      form: apiRequestArguments,
      json: true,
    }, function(error, response, body) {
      // checks for error returned from external:request
      if (error) {
        return reject(error);
      }
      // checks for correct response code
      if (response &&
          response.statusCode !== config.request.correctResponseStatusCode) {
        return reject(new errors.ResponseCodeError(response.statusCode));
      }
      // checks for api errors
      if (body && body.error) {
        return reject(new errors.APIError(body.code, body.error));
      }
      resolve(body ? helpers.parseResult(body.result) : null);
    });
  });
};

APIClient.errors = errors;
module.exports = APIClient;