// 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. You comment out the jquery include above and // then uncomment this stuff. //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) { var p = new HelixWebServicesClient.Models.Project(updated); deferred.resolve(p); }); ajax.fail(deferred.reject); return deferred.promise(); }, /** * Update an existing 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# */ updateSyncProject: function(project) { var ajax = authAjax({ client: this, method: 'PATCH', url: this.urlTo('/sync/v1/projects/' + encodeURIComponent(project.id)), data: JSON.stringify(project), contentType: 'application/json', dataType: 'json', processData: false }); var deferred = $.Deferred(); ajax.done(function(updated) { var p = new HelixWebServicesClient.Models.Project(updated); deferred.resolve(p); }); 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) { var instances = projects.map(function(p) { return new HelixWebServicesClient.Models.Project(p); }); deferred.resolve(instances); }); 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;
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 15741 | ptomiak | Branch HWS for my use. | ||
//guest/perforce_software/helix-web-services/main/source/helix_web_services_client_js/helix_web_services_client.js | |||||
#1 | 15622 | tjuricek |
Move source code to 'source/' subdirectory of branch. build/ will remain where it is. |
||
//guest/perforce_software/helix-web-services/main/helix_web_services_client_js/helix_web_services_client.js | |||||
#10 | 14177 | tjuricek | A wee bit of asciidoc conversion and some comment scoping | ||
#9 | 14176 | tjuricek |
Add a basic 'add member' function to the project details page. There appears to be some kind of funky caching issue with the components. The project data does get updated, so this UI is not 100% bug proof. Also, there's no autocomplete yet. |
||
#8 | 14175 | tjuricek |
Very basic read-only project details page. We're not going to display complete user detail information for a while. |
||
#7 | 14151 | tjuricek |
Add depot tree control and selection to the create projects page. Styling and error checking is kept to a minimum for the time being. Our goal is just internal workflow and feedback. |
||
#6 | 14108 | tjuricek |
Added models for handling Perforce server depot listing and traversal. This is not complete, however, the models are a start to making it easy to generate a tree control. (Most tree controls in the wild assume you know the tree structure from the start, which is not true in our case.) The tricky bit is making it easy to build the tree out given that you're visiting only one directory at a time. |
||
#5 | 14105 | tjuricek |
add 'listMy' and 'listAll' sync project variations to the JS API. Also, set up the project list to switch between the two project types. Also, setup each item in the project list to not be a grouped list item, but a custom view component. Not particularly useful, but it just displays a description now. |
||
#4 | 14002 | tjuricek | Some preliminary API documentation for the JavaScript SDK. | ||
#3 | 13999 | tjuricek |
Switching to use HWS JavaScript API for Store backend. Switching between node testing and browser testing environments isn't smooth yet. |
||
#2 | 13998 | tjuricek |
Add a basic ability for the JS SDK to create and list projects. Cleaned up some node madness on SDK initialization. |
||
#1 | 13976 | tjuricek |
Client SDK for JavaScript that logs in. This is currently tested via node using jasmine against a test environment, which requires some funky initialization. So this isn't a particularly useful client yet, but we'll need to add more error handling, etc, to smooth things out. |