@workfront/workfront-api
Version:
A Workfront API for the Node.js and the Web
686 lines (683 loc) • 29.8 kB
JavaScript
/**
* Copyright 2016 Workfront
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Hovhannes Babayan <bhovhannes at gmail dot com>
*/
/**
* Prefix for identifying a Sort field. Value is "_1_Sort" for first sort field, "_2_Sort", "_3_Sort" ... "_n_Sort".
* @readonly
* @type {String}
*/
/**
* Prefix for constants.
* @readonly
* @type {String}
*/
var INTERNAL_PREFIX = '$$';
/**
* Copyright 2015 Workfront
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Hovhannes Babayan <bhovhannes at gmail dot com>
* @author Sassoun Derderian <citizen.sas at gmail dot com>
*/
/**
* Configuration for the Api constructor
* @typedef {Object} config
* @property {String} url - Required. A url to Workfront server (for example: http://localhost:8080)
* @property {String} [version=internal] - Which version of api to use. At the moment of writing can be 2.0, 3.0, ..., 8.0. Pass 'unsupported' to use Workfront latest API (maybe unstable)
* @property {Boolean} [alwaysUseGet=false] - Will cause the api to make every request as a GET with params in the query string and add method=DESIRED_METHOD_TYPE in the query string. Some Workfront urls will have issues with PUT and DELETE calls if this value is false
* @property {String} [apiKey] - It is used to pass apiKey with every api request headers
* @property {Object} [headers] - An key-value object that sets custom headers (for example: {'user-agent': DESIRED_USER_AGENT_NAME})
*/
/**
* Creates new Api instance.
* @param {Object} config An object with the following keys:<br/>
* @constructor
*/
class Api {
constructor(config) {
this.serverAcceptsJSON = true;
this._uriGenerationMode = false;
this._httpOptions = {
url: config.url,
alwaysUseGet: config.alwaysUseGet,
headers: config.headers || {},
};
if (config.apiKey) {
this._httpOptions.headers.apiKey = config.apiKey;
}
// Append version to path if provided
let path;
const { version = 'internal', } = config;
if (['internal', 'unsupported', 'asp'].indexOf(version) >= 0) {
path = '/attask/api-' + version;
}
else {
path = '/attask/api/v' + version;
if (version === '2.0' || version === '3.0' || version === '4.0') {
this.serverAcceptsJSON = false;
}
}
this._httpOptions.path = path;
}
/**
* Used to obtain an API key
* @memberOf Api
* @param {String} username A username in Workfront
* @param {String} password Password to use
* @param {String} subdomain Sub-domain to use
* @return {Promise} A promise which will resolved with API key if everything went ok and rejected otherwise
*/
getApiKey(username, password, subdomain) {
const loginParams = {
username,
password,
};
if (subdomain !== undefined) {
loginParams['subdomain'] = subdomain;
}
return new Promise((resolve, reject) => {
if (typeof this._httpOptions.headers.apiKey !== 'undefined') {
resolve(this._httpOptions.headers.apiKey);
}
else {
const req = this.execute('USER', null, 'getApiKey', loginParams);
req.then((getApiKeyData) => {
if (getApiKeyData.result === '') {
const req2 = this.execute('USER', null, 'generateApiKey', loginParams);
req2.then((generateApiKeyData) => {
this._httpOptions.headers.apiKey = generateApiKeyData.result;
resolve(this._httpOptions.headers.apiKey);
}, reject);
}
else {
this._httpOptions.headers.apiKey = getApiKeyData.result;
resolve(this._httpOptions.headers.apiKey);
}
}, reject);
}
});
}
/**
* Copies an existing object with making changes on a copy.
* Copying is supported only for some objects. The {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} page displays which objects support the Copy action.
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {String} objID ID of object to copy
* @param {Object} updates Which fields to set on copied object. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @param {String|String[]} [fields] Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @param {String[]} options A list of options that are attached to the copy request (object specific)
* @return {Promise} A promise which will resolved with results if everything went ok and rejected otherwise
*/
copy(objCode, objID, updates, fields, options) {
const params = {
copySourceID: objID,
};
if (updates) {
params.updates = JSON.stringify(updates);
}
if (options) {
params.options = JSON.stringify(options);
}
return this.request(objCode, params, fields, Api.Methods.POST);
}
/**
* Used to retrieve number of objects matching given search criteria
* @memberOf Api
* @param {String} objCode
* @param {[Object]} query An object with search criteria
* @return {Promise}
*/
count(objCode, query) {
const req = this.request(objCode + '/count', query, null, Api.Methods.GET);
if (this._uriGenerationMode) {
return req;
}
return req.then(function (data) {
return data.count;
});
}
/**
* Invalidates the current API key.
* Call this to be able to retrieve a new one using getApiKey().
* @memberOf Api
* @return {Promise} A promise which will resolved if everything went ok and rejected otherwise
*/
clearApiKey() {
return new Promise((resolve, reject) => {
const req = this.execute('USER', null, 'clearApiKey');
req.then((result) => {
if (result) {
delete this._httpOptions.headers.apiKey;
resolve();
}
else {
reject();
}
});
});
}
/**
* Creates a new object.
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {Object} params Values of fields to be set for the new object. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @param {String|String[]} [fields] Which fields of newly created object to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @returns {Promise} A promise which will resolved with the ID and any other specified fields of newly created object
*/
create(objCode, params, fields) {
if (params.hasOwnProperty('updates')) {
return this.request(objCode, params, fields, Api.Methods.POST);
}
return this.request(objCode, { updates: params }, fields, Api.Methods.POST);
}
/**
* Edits an existing object
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {String} objID ID of object to modify
* @param {Object} updates Which fields to set. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @param {String|String[]} [fields] Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @return {Promise} A promise which will resolved with results if everything went ok and rejected otherwise
*/
edit(objCode, objID, updates, fields) {
if (updates.hasOwnProperty('updates')) {
return this.request(objCode + '/' + objID, updates, fields, Api.Methods.PUT);
}
return this.request(objCode + '/' + objID, { updates: updates }, fields, Api.Methods.PUT);
}
/**
* Edit multiple existing objects
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {Array} updates Array of fields for each object to be edited. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @param {String|String[]} [fields] Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @return {Promise} A promise which will resolved with results if everything went ok and rejected otherwise
*/
editMultiple(objCode, updates, fields) {
return this.request(objCode, { updates: updates }, fields, Api.Methods.PUT);
}
/**
* Executes an action for the given object
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {String|null} objID ID of object. Optional, pass null or undefined to omit
* @param {String} action An action to execute. A list of allowed actions are available within the {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} under "actions" for each object.
* @param {Object} [actionArgs] Optional. Arguments for the action. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of valid arguments
* @returns {Promise} A promise which will resolved if everything went ok and rejected otherwise
*/
execute(objCode, objID, action, actionArgs) {
let endPoint = objCode;
let params = { method: Api.Methods.PUT };
if (objID) {
endPoint += '/' + objID + '/' + action;
}
else {
params.action = action;
}
if (actionArgs) {
params = { ...params, ...actionArgs };
}
return this.request(endPoint, params, null, Api.Methods.POST);
}
/**
* Used for retrieve an object or multiple objects.
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {String|Array} objIDs Either one or multiple object ids
* @param {String|String[]} fields Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @return {Promise} A promise which will resolved with results if everything went ok and rejected otherwise
*/
get(objCode, objIDs, fields) {
if (typeof objIDs === 'string') {
objIDs = [objIDs];
}
let endPoint = objCode, params = null;
if (objIDs.length === 1) {
if (objIDs[0].indexOf(INTERNAL_PREFIX) === 0) {
params = { id: objIDs[0] };
}
else {
endPoint += '/' + objIDs[0];
}
}
else {
params = { id: objIDs };
}
return this.request(endPoint, params, fields, Api.Methods.GET);
}
/**
* Logs in into Workfront. Should be a first call to Workfront API.
* Other calls should be made after this one will be completed.
* @memberOf Api
* @param {String} username A username in Workfront
* @param {String} password Password to use
* @param {String} subdomain Sub-domain to use
* @return {Promise} A promise which will resolved with logged in user data if everything went ok and rejected otherwise
*/
login(username, password, subdomain) {
const params = { username, password };
if (subdomain !== undefined) {
params['subdomain'] = subdomain;
}
const req = this.request('login', params, null, Api.Methods.POST);
return req.then((data) => {
this.setSessionID(data.sessionID);
return data;
});
}
/**
* Logs out from Workfront
* @memberOf Api
* @return {Promise} A promise which will resolved if everything went ok and rejected otherwise
*/
logout() {
return new Promise((resolve, reject) => {
const req = this.request('logout', null, null, Api.Methods.GET);
req.then((result) => {
if (result && result.success) {
delete this._httpOptions.headers['X-XSRF-TOKEN'];
delete this._httpOptions.headers.sessionID;
resolve();
}
else {
reject();
}
});
});
}
/**
* Retrieves API metadata for an object.
* @memberOf Api
* @param {String} [objCode] One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}. If omitted will return list of objects available in API.
* @param {String|String[]} fields Which fields to return.
* @return {Promise} A promise which will resolved with object metadata if everything went ok and rejected otherwise
*/
metadata(objCode, fields) {
let path = '/metadata';
if (objCode) {
path = objCode + path;
}
return this.request(path, null, fields, Api.Methods.GET);
}
/**
* Executes a named query for the given obj code
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {String} query A query to execute. A list of allowed named queries are available within the {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} under "actions" for each object.
* @param {Object} [queryArgs] Optional. Arguments for the action. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of valid arguments
* @param {String|String[]} fields Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @returns {Promise} A promise which will resolved with received data if everything went ok and rejected with error info otherwise
*/
namedQuery(objCode, query, queryArgs, fields) {
return this.request(objCode + '/' + query, queryArgs, fields, Api.Methods.GET);
}
/**
* Deletes an object
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {String} objID ID of object
* @param {Boolean} [bForce] Pass true to cause the server to remove the specified data and its dependants
* @returns {Promise} A promise which will resolved if everything went ok and rejected otherwise
*/
remove(objCode, objID, bForce) {
const params = bForce ? { force: true } : null;
const req = this.request(objCode + '/' + objID, params, null, Api.Methods.DELETE);
if (this._uriGenerationMode) {
return req;
}
else {
return new Promise((resolve, reject) => {
req.then((result) => {
if (result && result.success) {
resolve();
}
else {
reject();
}
}, reject);
});
}
}
/**
* Performs report request, where only the aggregate of some field is desired, with one or more groupings.
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {Object} query An object with search criteria and aggregate functions
* @param {Boolean} [useHttpPost=false] Whenever to use POST to send query params
* @return {Promise} A promise which will resolved with results if everything went ok and rejected otherwise
*/
report(objCode, query, useHttpPost = false) {
let reportQuery, method;
if (useHttpPost) {
reportQuery = { ...query, method: Api.Methods.GET };
method = Api.Methods.POST;
}
else {
reportQuery = query;
method = Api.Methods.GET;
}
return this.request(objCode + '/report', reportQuery, null, method);
}
/**
* Do the request using Fetch API.
* @memberOf Api
* @param {String} path URI path where the request calls
* @param {Object} params An object with params
* @param {Object} [fields] Fields to query for the request
* @param {String} [method=GET] The method which the request will do (GET|POST|PUT|DELETE)
* @return {Promise} A promise which will resolved with results if everything went ok and rejected otherwise
*/
request(path, params, fields = [], method = Api.Methods.GET) {
const clonedParams = { ...params };
const options = this.getOptions(path, clonedParams, this._uriGenerationMode ? Api.Methods.GET : method);
const stringifiedFields = this.getFields(fields);
if (stringifiedFields) {
clonedParams.fields = stringifiedFields;
}
const headers = this.getHeaders();
const { bodyParams, queryString, contentType } = this.populateQueryStringAndBodyParams(clonedParams, options);
if (contentType) {
headers.append('Content-Type', contentType);
}
if (this._uriGenerationMode) {
let appendGetMethod = '';
if (queryString.indexOf('method=') === -1) {
appendGetMethod = (queryString === '' ? '?' : '&') + 'method=' + Api.Methods.GET;
}
// @ts-ignore-line
return path + queryString + appendGetMethod;
}
return makeFetchCall(options.url + options.path + queryString, {
headers,
body: bodyParams,
method: options.method,
});
}
/**
* Used for object retrieval by multiple search criteria.
* @memberOf Api
* @param {String} objCode One of object codes from {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer}
* @param {Object} [query] An object with search criteria
* @param {String|String[]} [fields] Which fields to return. See {@link https://developers.workfront.com/api-docs/api-explorer/|Workfront API Explorer} for the list of available fields for the given objCode.
* @param {Boolean} [useHttpPost=false] Whenever to use POST to send query params
* @return {Promise} A promise which will resolved with search results if everything went ok and rejected otherwise
*/
search(objCode, query, fields, useHttpPost = false) {
let searchQuery, method;
if (useHttpPost) {
searchQuery = { ...query, method: Api.Methods.GET };
method = Api.Methods.POST;
}
else {
searchQuery = query;
method = Api.Methods.GET;
}
return this.request(objCode + '/search', searchQuery, fields, method);
}
batch(uriCollector, isAtomic, isConcurrent) {
const batchApi = batchApiFactory(this);
const uris = uriCollector(batchApi);
if (uris.length === 0) {
return Promise.resolve(isAtomic ? undefined : []);
}
const req = this.request('/batch', {
atomic: !!isAtomic,
uri: uris,
concurrent: !!isConcurrent,
}, undefined, Api.Methods.POST);
if (isAtomic) {
return req.then((result) => {
if (result && result.success) {
return undefined;
}
throw new Error();
});
}
return req.then((results) => {
return results.map((resultItem) => resultItem.data);
});
}
/**
* Sets a current API key for future requests
* @memberOf Api
* @return {string} returns the given api key value
*/
setApiKey(apiKey) {
return (this._httpOptions.headers.apiKey = apiKey);
}
/**
* Sets a sessionID in the headers or removes sessionID if passed argument is undefined
* @memberOf Api
* @param {String|undefined} sessionID sessionID to set
*/
setSessionID(sessionID) {
if (sessionID) {
this._httpOptions.headers.sessionID = sessionID;
}
else {
delete this._httpOptions.headers.sessionID;
}
}
/**
* Sets a 'X-XSRF-TOKEN' in the headers or removes 'X-XSRF-TOKEN' if passed argument is undefined
* @memberOf Api
* @param {String|undefined} xsrfToken X-XSRF-TOKEN to set
*/
setXSRFToken(xsrfToken) {
if (xsrfToken) {
this._httpOptions.headers['X-XSRF-TOKEN'] = xsrfToken;
}
else {
delete this._httpOptions.headers['X-XSRF-TOKEN'];
}
}
uploadFileContent(fileContent, filename) {
const data = new FormData();
data.append('uploadedFile', fileContent, filename);
return this.request('upload', data, null, Api.Methods.POST);
}
getHeaders() {
const headers = new Headers();
headers.append('X-Requested-With', 'XMLHttpRequest');
if (this._httpOptions.headers.sessionID) {
headers.append('sessionID', this._httpOptions.headers.sessionID);
}
else if (this._httpOptions.headers['X-XSRF-TOKEN']) {
headers.append('X-XSRF-TOKEN', this._httpOptions.headers['X-XSRF-TOKEN']);
}
else if (this._httpOptions.headers.apiKey) {
headers.append('apiKey', this._httpOptions.headers.apiKey);
}
return headers;
}
getFields(fields) {
if (typeof fields === 'string') {
return fields;
}
if (Array.isArray(fields)) {
return fields.join(',');
}
}
getOptions(path, clonedParams, method) {
const options = { ...this._httpOptions };
if (options.alwaysUseGet && path !== 'login') {
clonedParams.method = method;
options.method = Api.Methods.GET;
}
else {
options.method = method;
}
if (path.indexOf('/') !== 0) {
path = '/' + path;
}
options.path = this._httpOptions.path + path;
return options;
}
populateQueryStringAndBodyParams(clonedParams, options) {
let bodyParams = null, queryString = '', contentType = null;
if (typeof FormData !== 'undefined' && clonedParams instanceof FormData) {
bodyParams = clonedParams;
}
else if (this.serverAcceptsJSON &&
typeof clonedParams.updates === 'object' &&
(options.method === Api.Methods.POST || options.method === Api.Methods.PUT)) {
contentType = 'application/json';
bodyParams = JSON.stringify(clonedParams.updates);
delete clonedParams.updates;
const qs = queryStringify(clonedParams);
if (qs) {
queryString = '?' + qs;
}
}
else {
contentType = 'application/x-www-form-urlencoded';
if (clonedParams.hasOwnProperty('updates') &&
typeof clonedParams.updates !== 'string') {
clonedParams.updates = JSON.stringify(clonedParams.updates);
}
bodyParams = queryStringify(clonedParams);
if (options.method === Api.Methods.GET || options.method === Api.Methods.DELETE) {
if (bodyParams) {
queryString = '?' + bodyParams;
}
bodyParams = null;
}
}
return {
bodyParams,
queryString,
contentType,
};
}
}
Api.Methods = {
GET: 'GET',
PUT: 'PUT',
DELETE: 'DELETE',
POST: 'POST',
};
const queryStringify = function (params) {
return Object.keys(params)
.reduce(function (a, k) {
if (Array.isArray(params[k])) {
params[k].forEach(function (param) {
a.push(k + '=' + encodeURIComponent(param));
});
}
else {
a.push(k + '=' + encodeURIComponent(params[k]));
}
return a;
}, [])
.join('&');
};
function batchApiFactory(api) {
const apiClone = Object.create(api);
apiClone._uriGenerationMode = true;
return {
copy: (objCode, objID, updates, fields, options) => {
return apiClone.copy(objCode, objID, updates, fields, options);
},
count: (objCode, query) => {
return apiClone.count(objCode, query);
},
create: (objCode, params, fields) => {
return apiClone.create(objCode, params, fields);
},
edit: (objCode, objID, updates, fields) => {
return apiClone.edit(objCode, objID, updates, fields);
},
editMultiple: (objCode, updates, fields) => {
return apiClone.editMultiple(objCode, updates, fields);
},
execute: (objCode, objID, action, actionArgs) => {
return apiClone.execute(objCode, objID, action, actionArgs);
},
get: (objCode, objIDs, fields) => {
return apiClone.get(objCode, objIDs, fields);
},
metadata: (objCode, fields) => {
return apiClone.metadata(objCode, fields);
},
namedQuery: (objCode, query, queryArgs, fields) => {
return apiClone.namedQuery(objCode, query, queryArgs, fields);
},
remove: (objCode, objID, bForce) => {
return apiClone.remove(objCode, objID, bForce);
},
report: (objCode, query) => {
return apiClone.report(objCode, query);
},
request: (path, params, fields, method = Api.Methods.GET) => {
return apiClone.request(path, params, fields, method);
},
search: (objCode, query, fields) => {
return apiClone.search(objCode, query, fields, false);
},
};
}
function makeFetchCall(url, fetchOptions) {
return fetch(url, { ...fetchOptions, credentials: 'same-origin' }).then(ResponseHandler.success, ResponseHandler.failure);
}
const ResponseHandler = {
success: (response) => {
if (response.ok) {
return response.json().then((data) => {
if (data.error) {
throw {
status: response.status,
message: data.error.message,
};
}
return data.data;
});
}
else {
return response.json().then((data) => {
throw {
status: response.status,
message: data.error.message,
};
}, () => {
throw {
status: response.status,
message: response.statusText,
};
});
}
},
failure: (err) => {
throw {
message: err.message || err.statusText,
};
},
};
export { Api, ResponseHandler, makeFetchCall };
//# sourceMappingURL=workfront-api.es.js.map