Source: helix_web_services_client.js

// The Client object is how people should access the Helix Web Services server
// via AJAX calls (that uses jQuery).
//
// The API here is promise-oriented, which makes it slightly different to work
// with than the Ruby or Qt SDKs.


// Initialize jQuery that can be tested on node.js or run in a browser.
var $ = require('jquery');

// When executing tests, this needs to be used to initialize jquery, which...
// is wonderfully automated.
//if (typeof(process) == 'undefined') {
//  $ = require('jquery');
//} else {
//  $ = require('jquery')(require("jsdom").jsdom().parentWindow);
//
//  var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
//
//  $.support.cors = true;
//  $.ajaxSettings.xhr = function () {
//    return new XMLHttpRequest();
//  };
//}
//if (typeof(btoa) == 'undefined') {
//  btoa = require('btoa');
//}

var assign = require('object-assign');
require('./polyfill');

// Construct a new HelixWebServicesClient interface
/**
 * Create the main client object used to communicate via remote calls.
 *
 * @param {Object} options Set various properties on this object.
 * @constructor
 */
function HelixWebServicesClient(options) {
  options = options || {};
  /**
   * The base URL, e.g. `http://example.com` of the web server.
   *
   * @type {string}
   */
  this.url = options.url;
  /**
   * The Perforce user name.
   *
   * @type {string}
   */
  this.user = options.user;
  /**
   * If accessing an HWS instance behind a proxy, it's probably available
   * underneath a subdirectory
   *
   * @type {string}
   */
  this.prefix = options.prefix || "";
  /**
   * Set a security token if you know it. This is used for authenticated
   * methods.
   *
   * @type {string}
   */
  this.session_token = options.session_token;

  this._expiredCallbacks = [];
}

// Use our main constructor as a namespace for exporting models.
HelixWebServicesClient.Models = require('./models');

//-------------------------------------------------------------------------
// Local (private) Methods
//-------------------------------------------------------------------------

// Private method that will call jQuery.ajax and return it's promise.
//
// Options:
// - client
// - method
// - url
// - ... and others, passed to jQuery.ajax directly
function authAjax(options) {
  var ajaxOpts = {
    beforeSend: function (xhr) {
      var auth = btoa(options.client.user + ":" + options.client.session_token);
      xhr.setRequestHeader("Authorization", "Basic " + auth);
    }
  };

  // Other options are just included into ajaxOpts
  copy = assign({}, options);
  delete copy.client;

  assign(ajaxOpts, copy);

  var ajax = $.ajax(ajaxOpts);

  // We check for session expiration failures and trigger a sepcial handler on
  // our client.

  var deferred = $.Deferred();

  ajax.done(deferred.resolve);

  ajax.fail(function(jqXHR, textStatus, error) {
    if (jqXHR.status == 403) {
      options.client._expiredCallbacks.forEach(function(cb) { cb.call(); });
    } else {
      deferred.reject(jqXHR, textStatus, error);
    }
  });

  return deferred.promise();
}

// Will alter the path, encoding each subpath component along the route,
// and encode it to become a path parameter.
function normalizePath(path) {
  if (!path) {
    return '';
  }
  if (path.startsWith('//')) {
    path = path.substring(2);
  }
  parts = path.split('/');
  return parts.map(function(p) { return encodeURIComponent(p); }).join('/');
}


assign(HelixWebServicesClient.prototype, {

  //-------------------------------------------------------------------------
  // Callbacks
  //-------------------------------------------------------------------------

  /**
   * When your session expires on any particular call, this callback will get
   * triggered.
   *
   * @param {function} callback A function() {} that gets called back if the
   *                            session is no longer valid.
   */
  addSessionExpiredHandler: function(callback) {
    this._expiredCallbacks.push(callback);
  },

  //-------------------------------------------------------------------------
  // Asynchronous Methods
  //-------------------------------------------------------------------------

  /**
   * Creates a new session token by logging into the server.
   *
   * @param {String} [user] Optional attribute. If you specify it, we'll cache it as the
   *  `user` property.
   * @param {String} password Required Perforce password for the user.
   * @returns {Promise.<String>|Promise.<jqXHR, String, error>} On success,
   *  you'll receive the security token, which will be cached on this client
   *  object as the `session_token` property.
   * @memberOf! HelixWebServicesClient#
   */
  logIn: function (user, password) {
    if (!password) {
      password = user;
      user = this.user;
    }

    // TODO we very likely want to configure ajax for common error handling
    var ajax = $.post(
      this.urlTo('/auth/v1/sessions'),
      {user: this.user, password: password}
    );

    // We wrap the promise in order to cache the session before done() is
    // invoked.
    var deferred = $.Deferred();

    var self = this;

    ajax.done(function (data, textStatus, jqXHR) {
      self.session_token = data;
      deferred.resolve(self.session_token);
    });

    ajax.fail(function (jqXHR, textStatus, error) {
      deferred.reject(jqXHR, textStatus, error);
    });

    return deferred.promise();
  },

  /**
   * Destroys the session (on the server)
   *
   * @returns {Promise|Promise<jqXHR, textStatus, error>}
   * @memberOf! HelixWebServicesClient#
   */
  logOut: function () {

    var ajax = authAjax({
      client: this,
      method: 'DELETE',
      url: this.urlTo('/auth/v1/sessions/' + this.session_token)
    });

    var deferred = $.Deferred();

    ajax.done(function () {
      deferred.resolve();
    });

    ajax.fail(deferred.reject);

    return deferred.promise();
  },

  /**
   * List files at the particular directory level.
   *
   * @param {String} path The directory to list. Should be an absolute depot
   *                      path, e.g., `//depot/dirA`. When empty, this lists
   *                      depots in the system.
   *
   * @returns {Promise.<Array.<PathItem>>|Promise.<jqXHR, String, error>}
   *    Promises resolve to a list of PathItem models or the error data. Each
   *    PathItem is either all depot paths (if `path` is empty) or the child
   *    Directory or Files of `path`.
   *
   * @memberOf! HelixWebServicesClient#
   */
  listFiles: function (path) {
    var subPath = normalizePath(path);

    var ajax = authAjax({
      client: this,
      method: 'GET',
      url: this.urlTo('/perforce/v1/files/' + subPath),
      dataType: 'json'
    });

    var deferred = $.Deferred();

    ajax.done(function (items) {
      var arr = HelixWebServicesClient.Models.PathItem.fromArray(items);
      deferred.resolve(arr);
    });

    ajax.fail(deferred.reject);

    return deferred.promise();
  },

  /**
   * Create a new Helix Sync project.
   *
   * @param {Object|Project} project
   * @returns {Promise.<Project>|Promise.<jqXHR, String, error>} Will return
   *  the 'updated' project structure, which likely has several default
   *  configuration values set.
   * @memberOf! HelixWebServicesClient#
   */
  createSyncProject: function (project) {
    var ajax = authAjax({
      client: this,
      method: 'POST',
      url: this.urlTo('/sync/v1/projects'),
      data: JSON.stringify(project),
      contentType: 'application/json',
      dataType: 'json',
      processData: false
    });

    var deferred = $.Deferred();

    ajax.done(function (updated) {
      deferred.resolve(updated);
    });

    ajax.fail(deferred.reject);

    return deferred.promise();
  },

  /**
   * List the Helix Sync projects the current user is a member of.
   *
   * @returns {*|Promise.<Array.<Project>>|Promise.<jqXHR, textStatus, error>}
   * @memberOf! HelixWebServicesClient#
   */
  listMySyncProjects: function () {
    return this.syncProjects({members: this.user});
  },

  /**
   * List all Helix Sync projects on the server.
   *
   * @returns {*|Promise.<Array.<Project>>|Promise.<jqXHR, textStatus, error>}
   * @memberOf! HelixWebServicesClient#
   */
  listAllSyncProjects: function () {
    return this.syncProjects();
  },

  // TODO this should resolve to an array of our project models instead of an array-like Object
  /**
   * Fetch a listing of Helix Sync projects from the server.
   *
   * @param {Object} [options] Set 'listType'
   * @returns {Promise.<Array<Project>>|Promise<jqXHR, textStatus, error>}
   * @memberOf! HelixWebServicesClient#
   */
  syncProjects: function (options) {

    var ajaxOptions = {
      client: this,
      method: 'GET',
      url: this.urlTo('/sync/v1/projects'),
      dataType: 'json'
    };

    if (options)
      ajaxOptions['data'] = options;

    var ajax = authAjax(ajaxOptions);

    var deferred = $.Deferred();

    ajax.done(function (projects) {
      deferred.resolve(projects);
    });

    ajax.fail(deferred.reject);

    return deferred.promise();
  },

  //-------------------------------------------------------------------------
  // Helper Methods
  //-------------------------------------------------------------------------
  // These methods shouldn't return Promise interfaces.

  urlTo: function (path) {
    var url = this.url || "";
    if (this.prefix) {
      url += this.prefix;
    }
    url += path;
    return url;
  }
});

module.exports = HelixWebServicesClient;