UNPKG

pivotal

Version:
928 lines (680 loc) 24.9 kB
/** # node-pivotal : Node.JS API Library for PivotalTracker License: MIT To install: npm install pivotal ## To use: ```javascript var pivotal = require("pivotal"); pivotal.useToken("myToken"); ``` You can also retrieve the token initially by using the pivotal.getToken function ## Contributing When making changes to the library, please run `make doc` before submitting pull requests. ## Tests Tests are located in test/ directory. To run them you need to install the required dependencies ```shell npm install ``` and either set your pivotal api-token as a variable ```shell export token=xxxxxxxxxxxxxx ``` or your username and password (which will result in an additional test for creating the token) ```shell export username=tester export password=testpw ``` for testing against a specific project/story/member ```shell export project_id=123456 export story_id=9876546 export member_id=192837 ``` finally run the tests using: ```shell make test ``` ## For more information: - [PivotalTracker API v3 Documentation](https://www.pivotaltracker.com/help/api?version=v3 "PivotalTracker") ## API methods For examples see tests/test.js */ var xml2js = require("xml2js"), url = require("url"), fs = require("fs"), querystring = require("querystring"), sanitize = require("validator").sanitize, https = require("https"); /* Switching debug to true will print output to the console. Only useful for debugging this API */ var pivotal = { debug: false }; /** ### pivotal.getToken : retrieve a user's token ref: https://www.pivotaltracker.com/help/api?version=v3#retrieve_token_post __Arguments__ + user : user name (email) + pass : the user's password */ pivotal.getToken = function (user, pass, cb) { pivotal.apiCall("POST", ["tokens", "active"], null, querystring.stringify({"username": user, "password" : pass}), null, cb); }; /** ### pivotal.useToken : set the token to use for all Pivotal callso __Arguments__ + token: A valid Pivotal Token */ pivotal.useToken = function (token) { this.token = token; }; /** ### pivotal.getActivities: list activities for the projects you have access to ref: https://www.pivotaltracker.com/help/api?version=v3#get_all_activity __Arguments__ + filters : Limits the return data ```javascript { project (int) : project id limit : maximum return entries occurred_since_date : earliest date for return entries newer_than_version : allows restricting the activity feed to only those items that have a greater than supplied version } ``` */ pivotal.getActivities = function (filters, cb) { var url = ["activities"]; if (filters && filters.project) { url.unshift(filters.project); url.unshift("projects"); delete filters.project; } pivotal.apiCall("GET", url, filters, null, null, cb); }; /** ### pivotal.getProjectActivities: list activities for the projects you have access to ref: https://www.pivotaltracker.com/help/api?version=v3#get_project_activity __Arguments__ + project (int) : project id + filters : Limits the return data ```javascript { limit : maximum return entries occurred_since_date : earliest date for return entries newer_than_version : allows restricting the activity feed to only those items that have a greater than supplied version } ``` */ pivotal.getProjectActivities = function (projectId, filters, cb) { filters = filters || {}; filters.project = projectId; pivotal.getActivities(filters, cb); }; /** ### pivotal.getProjects : get all the projects you have access to ref: https://www.pivotaltracker.com/help/api?version=v3#get_project_all_projects */ pivotal.getProjects = function (cb) { pivotal.apiCall("GET", ["projects"], null, null, null, cb); }; /** ### pivotal.getProject : access a project ref: https://www.pivotaltracker.com/help/api?version=v3#get_project_info __Arguments__ + id (int) : id of the project */ pivotal.getProject = function (projectId, cb) { pivotal.apiCall("GET", ["projects", projectId], null, null, null, cb); }; /** ### pivotal.addProject : create a project ref: https://www.pivotaltracker.com/help/api?version=v3#add_project __Arguments__ + project : Data of the project to add ```javascript { name (string) : The project's name iteration_length (int) : Iteration length no_owner (boolean, optional) : Does the project have an owner? } ``` */ pivotal.addProject = function (projectData, cb) { if (typeof projectData.no_owner === 'undefined') { projectData.no_owner = true; } pivotal.apiCall("POST", ["projects"], null, { project : projectData }, null, cb); }; /** ### pivotal.getMemberships : get the members of this projects ref: https://www.pivotaltracker.com/help/api?version=v3#get_memberships __Arguments__ + projectId (int) : id of the project */ pivotal.getMemberships = function (projectId, cb) { pivotal.apiCall("GET", ["projects", projectId, "memberships"], null, null, null, cb); }; /** ### pivotal.getMembership : get a single member of this projects ref: https://www.pivotaltracker.com/help/api?version=v3#get_membership_info __Arguments__ + projectId (int) : id of the project + membershipId (int) : id of the member */ pivotal.getMembership = function (projectId, membershipId, cb) { pivotal.apiCall("GET", ["projects", projectId, "memberships", membershipId], null, null, null, cb); }; /** ### pivotal.addMembership : add a member to this projects ref: https://www.pivotaltracker.com/help/api?version=v3#add_membership __Arguments__ + projectId (int) : id of the project + membershipData : Data of the new member ```javascript { role : Member or Owner person : Personal information { name : Person's full name initials : Name's initials email : E-mail } } ``` __Note__: The user does not have to be in the system already. He will receieve an email asking him to join if he does not have a project already. */ pivotal.addMembership = function (projectId, membershipData, cb) { pivotal.apiCall("POST", ["projects", projectId, "memberships"], null, { "membership" : membershipData }, null, cb); }; /** ### pivotal.removeMembership : remove a single member of this projects ref: https://www.pivotaltracker.com/help/api?version=v3#remove_membership __Arguments__ + projectId (int) : id of the project + membershipId (int) : id of the member */ pivotal.removeMembership = function (projectId, membershipId, cb) { pivotal.apiCall("DELETE", ["projects", projectId, "memberships", membershipId], null, null, null, cb); }; /** ### pivotal.getIterations : get a project list of iterations ref: https://www.pivotaltracker.com/help/api?version=v3#get_iterations __Arguments__ + projectId (int) : id of the project + filters : Limits the return data ```javascript { group : filter by iteration type [done|current|backlog|current_backlog] limit : maximum return entries offset : start from story num. N in the list } ``` */ pivotal.getIterations = function (projectId, filters, cb) { var url = ["projects", projectId, "iterations"]; if (filters && filters.group) { url.push(filters.group); delete filters.group; } pivotal.apiCall("GET", url, filters, null, null, cb); }; /** ### pivotal.getDoneIterations: get a project completed iterations ref: https://www.pivotaltracker.com/help/api?version=v3#get_iterations __Arguments__ + projectId (int) : id of the project + filters : Limits the return data ```javascript { limit : maximum return entries offset : start from story num. N in the list } ``` */ pivotal.getDoneIterations = function (projectId, filters, cb) { filters = filters || {}; filters.group = "done"; pivotal.getIterations(projectId, filters, cb); }; /** ### pivotal.getCurrentIteration : get a project current iteration ref: https://www.pivotaltracker.com/help/api?version=v3#get_iterations __Arguments__ + projectId (int) : id of the project */ pivotal.getCurrentIteration = function (projectId, cb) { pivotal.getIterations(projectId, { group: "current" }, cb); }; /** ### pivotal.getBacklogIterations: get a project backlog ref: https://www.pivotaltracker.com/help/api?version=v3#get_iterations __Arguments__ + projectId (int) : id of the project + filters : Limits the return data ```javascript { limit : maximum return entries offset : start from story num. N in the list } ``` */ pivotal.getBacklogIterations = function (projectId, filters, cb) { filters = filters || {}; filters.group = "backlog"; pivotal.getIterations(projectId, filters, cb); }; /** ### pivotal.getCurrentBacklogIterations: get a project backlog and current iterations ref: https://www.pivotaltracker.com/help/api?version=v3#get_iterations __Arguments__ + projectId (int) : id of the project */ pivotal.getCurrentBacklogIterations = function (projectId, cb) { pivotal.apiCall("GET", ["projects", projectId, "iterations"], { group: "current_backlog" }, null, null, cb); }; /** ### pivotal.getStories: Get a list of stories for this project ref: https://www.pivotaltracker.com/help/api?version=v3#get_all_stories __Arguments__ + projectId (int) : id of the project + filters : Limits the return data ```javascript { limit : maximum return entries offset : start from story num. N in the list filter : search string to use (ex: filter=label:"needs feedback" type:bug) } ``` */ pivotal.getStories = function (projectId, filters, cb) { function isEmptyObject (obj) { for (var d in obj){ return false; } return true; } pivotal.apiCall("GET", ["projects", projectId, "stories"], filters, null, null, function (err, data) { if (err) { cb(err); } if (isEmptyObject(data)) { data = {story: []} } else if (isEmptyObject(data.story)) { data.story = []; } else if (!Array.isArray(data.story)) { data.story = [data.story]; } cb(null, data); }); }; /** ### pivotal.getStory: Get a story from a project ref: https://www.pivotaltracker.com/help/api?version=v3#get_story __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story */ pivotal.getStory = function (projectId, storyId, cb) { pivotal.apiCall("GET", ["projects", projectId, "stories", storyId], null, null, null, cb); }; /** ### pivotal.addStory: Add a story to a project ref: https://www.pivotaltracker.com/help/api?version=v3#add_story and https://www.pivotaltracker.com/help/api?version=v3#link_story __Arguments__ + projectId (int) : id of the project + storyData : data of the story ```javascript { name : Name of this story story_type : bug, feature, chore, release estimate (int) : number which indicates the level of difficulty of the story description : description, labels : Comma-separated list of labels requested_by : Name of the requester (should be an existing member person name, but I dont know if this is an actual limitation) } ``` */ pivotal.addStory = function (projectId, storyData, cb) { pivotal.apiCall("POST", ["projects", projectId, "stories"], null, { story : storyData }, null, cb); }; /** ### pivotal.addStoryAttachment: Add a file to a story ref: https://www.pivotaltracker.com/help/api?version=v3#upload_attachment __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story + fileData : information of file to upload ```javascript { name : filename of the file after upload path : path to the file on disk data : if no path is provided, one may simply put the data of the file in there instead } ``` */ pivotal.addStoryAttachment = function (projectId, storyId, fileData, cb) { pivotal.apiCall("POST", ["projects", projectId, "stories", storyId, "attachments"], null, null, fileData, cb); }; /** ### pivotal.addStoryComment: Add a comment to a story ref: https://www.pivotaltracker.com/help/api?version=v3#add_note __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story + comment (string) : The text of the comment to add */ pivotal.addStoryComment = function (projectId, storyId, comment, cb) { pivotal.apiCall("POST", ["projects", projectId, "stories", storyId, "notes"], null, { note: { text : comment } }, null, cb); }; /** ### pivotal.updateStory: Update story infos and/or move a story to a different project ref: https://www.pivotaltracker.com/help/api?version=v3#update_story __Arguments__ + projectId (int) : id of the project + storyData : data of the story ```javascript { project_id : Id of the project name : Name of this story story_type : bug, feature, chore, release estimate (int) : number which indicates the level of difficulty of the story description : description, labels : Comma-separated list of labels requested_by : Name of the requester (should be an existing member person name, but I dont know if this is an actual limitation) } ``` */ pivotal.updateStory = function (projectId, storyId, storyData, cb) { pivotal.apiCall("PUT", ["projects", projectId, "stories", storyId], null, { story : storyData }, null, cb); }; /** ### pivotal.moveStory: move a story in the list of priority ref: https://www.pivotaltracker.com/help/api?version=v3#move_stories __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story + moveData : Information on how the move should happen ```javascript { target : Id of the destination story move : before or after (the target story) } ``` */ pivotal.moveStory = function (projectId, storyId, moveData, cb) { var postData = {}, m = null; for (m in moveData) { if(moveData.hasOwnProperty(m)) { postData["move[" + m + "]"] = moveData[m]; } } pivotal.apiCall("POST", ["projects", projectId, "stories", storyId, "moves"], postData, null, null, cb); }; /** ### pivotal.removeStory: remove a story from a project ref: https://www.pivotaltracker.com/help/api?version=v3#delete_story __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story */ pivotal.removeStory = function (projectId, storyId, cb) { pivotal.apiCall("DELETE", ["projects", projectId, "stories", storyId], null, null, null, cb); }; /** ### pivotal.deliverAllFinishedStories: self-explanatory ref: https://www.pivotaltracker.com/help/api?version=v3#deliver_all_finished __Arguments__ + projectId (int) : id of the project */ pivotal.deliverAllFinishedStories = function (projectId, cb) { pivotal.apiCall("PUT", ["projects", projectId, "stories", "deliver_all_finished"], null, null, null, cb); }; /** ### pivotal.getTasks: Get tasks for a story ref: https://www.pivotaltracker.com/help/api?version=v3#view_task __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story */ pivotal.getTasks = function (projectId, storyId, cb) { pivotal.apiCall("GET", ["projects", projectId, "stories", storyId, "tasks"], null, null, null, cb); }; /** ### pivotal.getTask: Get a task from a story ref: https://www.pivotaltracker.com/help/api?version=v3#view_task __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story + taskId (int) : id of the task */ pivotal.getTask = function (projectId, storyId, taskId, cb) { pivotal.apiCall("GET", ["projects", projectId, "stories", storyId, "tasks", taskId], null, null, null, cb); }; /** ### pivotal.addTask : add a task to a story ref: https://www.pivotaltracker.com/help/api?version=v3#add_task __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story + taskData : data of the task ```javascript { description : text of the task complete : true of false } ``` */ pivotal.addTask = function (projectId, storyId, taskData, cb) { pivotal.apiCall("POST", ["projects", projectId, "stories", storyId, "tasks"], null, { task : taskData }, null, cb); }; /** ### pivotal.updateTask : add a task to a story ref: https://www.pivotaltracker.com/help/api?version=v3#update_task __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story + taskId (int) : id of the task + taskData : data of the task ```javascript { description : text of the task complete : true of false } ``` */ pivotal.updateTask = function (projectId, storyId, taskId, taskData, cb) { pivotal.apiCall("PUT", ["projects", projectId, "stories", storyId, "tasks", taskId], null, { task : taskData }, null, cb); }; /** ### pivotal.removeTask : remove a task from a story ref: https://www.pivotaltracker.com/help/api?version=v3#delete_task __Arguments__ + projectId (int) : id of the project + storyId (int) : id of the story + taskId (int) : id of the task */ pivotal.removeTask = function (projectId, storyId, taskId, cb) { pivotal.apiCall("DELETE", ["projects", projectId, "stories", storyId, "tasks", taskId], null, null, null, cb); }; pivotal.apiCall = function (method, pathSegments, query, data, file, cb) { if (data && file) { throw new Error("The Pivotal API does not support file upload and XML POSTing at the same time"); } // Build Request URL var path = "/services/v3/" + pathSegments.join("/"), postData = null, options = null, req = null, boundaryKey = null; // Append query string if (query) { path += "?" + querystring.stringify(query); } // Request options options = { headers : { "X-TrackerToken" : this.token, "Accept" : "text/html,application/xhtml+xml,application/xml", "Host" : "www.pivotaltracker.com", "Connection" : "keep-alive", "Content-Length" : 0 }, host : "www.pivotaltracker.com", path : path, method : method }; // format data if required if (data) { if(typeof(data) === "string"){ postData = data; } else { postData = this.toXml(data); options.headers["Content-Type"] = "application/xml"; } options.headers["Content-Length"] = postData.length; } if (file) { boundaryKey = Math.random().toString(12).substr(2,12); if (file.path) { options.headers["Content-Length"] = fs.statSync(file.path).size; } else { options.headers["Content-Length"] = file.data.length; } options.headers["Content-Length"] += 196 + file.name.length; options.headers.Expect = "100-continue"; options.headers["Content-Type"] = "multipart/form-data; boundary=----------------------------" + boundaryKey; } // Create request pivotal.log("requesting", options.path, options.method, data); req = https.request(options, function (res) { var content = "", parser = new xml2js.Parser({ explicitArray : false, ignoreAttrs : true, explicitRoot : false }); res.on('data', function (chunk) { content += chunk; }); res.on('end', function(){ if (this.statusCode !== 200) { return cb({code: this.statusCode, desc: "API returned an HTTP error"}, null); } if (content === "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<nil-classes type=\"array\"/>\n") { return cb(null, null); } parser.parseString(content, function (err, ret) { pivotal.log("info", "Result:", content); if (err) { pivotal.log("error", "Result:", content); err = { "errors" : { "error" : [ "Error while parsing PT HTTP service response", err ] } }; } if (ret && ret.errors) { err = ret; ret = null; } // // We do this so that any error generated by the callback function // will not bubble up and trigger the on('error') of the parser. // process.nextTick(function () { cb(err, ret); }); }); }); }); // Error Reporting req.on('error', function(e) { cb({ "errors" : { "error" :[e] } }, null); }); // PUT / POST data if (postData) { return req.end(postData); } /* Upload data file should be an object with the following structure { "name" : "myfilename.ext", "data" : somedatawhatever, "path" : "/path/to/the/file.ext" } If no data is provided, we will look for the file on the disk at the location given in path; This currently limits to one upload at a time, but could be adapted in the case where the Pivotal API would start to have multiple-upload capacities */ if (!file) { return req.end(); } req.write("------------------------------" + boundaryKey + "\r\n"); req.write("Content-Disposition: form-data; name=\"Filedata\"; filename=\"" + file.name + "\"\r\n"); req.write("Content-Type: application/octet-stream\r\n\r\n"); // If the file object holds the data, we write and close if (file.data) { req.write(file.data); return req.end("\r\n------------------------------" + boundaryKey + "--\r\n"); } // If we have a path, we stream the content to HTTP and // close the req object once were done. return fs.createReadStream(file.path, { bufferSize: 4 * 1024 }) .on("end", function(){ return req.end("\r\n------------------------------" + boundaryKey + "--\r\n"); }) .pipe(req, { end: false }); }; // Debug logging pivotal.log = function(){ if (this.debug) { console.log.apply(null, arguments); } }; // Format a JS object to XML string pivotal.toXml = function (data) { var ret = "", val = null, d = null, t = '', fieldTypes = { 'no_owner': 'type=\"boolean\"' }; for(d in data){ if (data.hasOwnProperty(d)) { if (typeof(data[d]) === "object") { val = this.toXml(data[d]); } else { val = sanitize(data[d].toString()).entityEncode(); } if (fieldTypes[d] !== undefined) { t = fieldTypes[d]; } ret += "<" + d + ' ' + t + ">" + val + "</" + d + ">"; } } return ret; }; module.exports = pivotal;