jira.js

JavaScript JIRA API for node.js

Build Status

A node.js module, which provides an object oriented wrapper for the JIRA REST API.

This library is built to support version 2.0.alpha1 of the JIRA REST API. This library is also tested with version 2 of the JIRA REST API It has been noted that with Jira OnDemand, 2.0.alpha1 does not work, devs should revert to 2. If this changes, please notify us.

JIRA REST API documentation can be found here

Installation

Install with the node package manager npm:

$ npm install jira

or

Install via git clone:

$ git clone git://git://github.com/steves/node-jira.git
$ cd node-jira
$ npm install

Example

Find the status of an issue.

JiraApi = require('jira').JiraApi;

var jira = new JiraApi('https', config.host, config.port, config.user, config.password, '2.0.alpha1');
jira.findIssue(issueNumber, function(error, issue) {
    console.log('Status: ' + issue.fields.status.value.name);
});

Currently there is no explicit login call necessary as each API call uses Basic Authentication to authenticate.

Options

JiraApi options: * protocol<string>: Typically 'http:' or 'https:' * host<string>: The hostname for your jira server * port<int>: The port your jira server is listening on (probably 80 or 443) * user<string>: The username to log in with * password<string>: Keep it secret, keep it safe * Jira API Version<string>: Known to work with 2 and 2.0.alpha1 * verbose<bool>: Log some info to the console, usually for debugging * strictSSL<bool>: Set to false if you have self-signed certs or something non-trustworthy

Implemented APIs

  • Authentication
  • Projects
    • Pulling a project
    • List all projects viewable to the user
  • Versions
    • Pulling versions
    • Adding a new version
    • Pulling unresolved issues count for a specific version
  • Rapid Views
    • Find based on project name
    • Get the latest Green Hopper sprint
    • Gets attached issues
  • Issues
    • Add a new issue
    • Update an issue
    • Transition an issue
    • Pulling an issue
    • Issue linking
    • Add an issue to a sprint
    • Get a users issues (open or all)
    • List issue types
    • Search using jql
    • Set Max Results
    • Set Start-At parameter for results
    • Add a worklog
  • Transitions
    • List

TODO

  • Refactor currently implemented APIs to be more Object Oriented
  • Refactor to make use of built-in node.js events and classes

Changelog

  • 0.5.0 Last param is now for strict SSL checking, defaults to true
  • 0.4.1 Now handing errors in the request callback (thanks mrbrookman)
  • 0.4.0 Now auto-redirecting between http and https (for both GET and POST)
  • 0.3.1 Request is broken, setting max request package at 2.15.0
  • 0.3.0 Now Gets Issues for a Rapidview/Sprint (thanks donbonifacio)
  • 0.2.0 Now allowing startAt and MaxResults to be passed to searchJira, switching to semantic versioning.
  • 0.1.0 Using Basic Auth instead of cookies, all calls unit tested, URI creation refactored
  • 0.0.6 Now linting, preparing to refactor
  • 0.0.5 JQL search now takes a list of fields
  • 0.0.4 Added jql search
  • 0.0.3 Added APIs and Docco documentation
  • 0.0.2 Initial version
var url = require('url'),
    logger = console;


var JiraApi = exports.JiraApi = function(protocol, host, port, username, password, apiVersion, verbose, strictSSL) {
    this.protocol = protocol;
    this.host = host;
    this.port = port;
    this.username = username;
    this.password = password;
    this.apiVersion = apiVersion;

Default strictSSL to true (previous behavior) but now allow it to be modified

    if (strictSSL == null) {
        strictSSL = true;
    }
    this.strictSSL = strictSSL;

This is so we can fake during unit tests

    this.request = require('request');
    if (verbose !== true) { logger = { log: function() {} }; }

This is the same almost every time, refactored to make changing it later, easier

    this.makeUri = function(pathname, altBase) {
        var basePath = 'rest/api/';
        if (altBase != null) {
            basePath = altBase;
        }

        var uri = url.format({
            protocol: this.protocol,
            hostname: this.host,
            auth: this.username + ':' + this.password,
            port: this.port,
            pathname: basePath + this.apiVersion + pathname
        });
        return uri;
    };


};

(function() {

Find an issue in jira

Takes

  • issueNumber: the issueNumber to find
  • callback: for when it's done

Returns

  • error: string of the error
  • issue: an object of the issue

Jira Doc

    this.findIssue = function(issueNumber, callback) {

        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issue/' + issueNumber),
            method: 'GET'
        };

        this.request(options, function(error, response, body) {
        
            if (error) {
                callback(error, null);
                return;
            }
            
            if (response.statusCode === 404) {
                callback('Invalid issue number.');
                return;
            }

            if (response.statusCode !== 200) {
                callback(response.statusCode + ': Unable to connect to JIRA during findIssueStatus.');
                return;
            }

            callback(null, JSON.parse(body));

        });
    };

Get the unresolved issue count

Takes

  • version: version of your product that you want issues against
  • callback: function for when it's done

Returns

  • error: string with the error code
  • count: count of unresolved issues for requested version

Jira Doc

    
    this.getUnresolvedIssueCount = function(version, callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/version/' + version + '/unresolvedIssueCount'),
            method: 'GET'
        };

        this.request(options, function(error, response, body) {
           
            if (error) {
                callback(error, null);
                return;
            }
          
            if (response.statusCode === 404) {
                callback('Invalid version.');
                return;
            }

            if (response.statusCode !== 200) {
                callback(response.statusCode + ': Unable to connect to JIRA during findIssueStatus.');
                return;
            }

            body = JSON.parse(body);
            callback(null, body.issuesUnresolvedCount);
            
        });
    };

Get the Project by project key

Takes

  • project: key for the project
  • callback: for when it's done

Returns

  • error: string of the error
  • project: the json object representing the entire project

Jira Doc

    this.getProject = function(project, callback) {

        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/project/' + project),
            method: 'GET'
        };

        this.request(options, function(error, response, body) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 404) {
                callback('Invalid project.');
                return;
            }

            if (response.statusCode !== 200) {
                callback(response.statusCode + ': Unable to connect to JIRA during getProject.');
                return;
            }

            body = JSON.parse(body);
            callback(null, body);
            
        });
    };

Find the Rapid View for a specified project

Takes

  • projectName: name for the project
  • callback: for when it's done

Returns

  • error: string of the error
  • rapidView: rapid view matching the projectName
    
    /**
     * Finds the Rapid View that belongs to a specified project.
     *
     * @param projectName
     * @param callback
     */
    this.findRapidView = function(projectName, callback) {

        var options = {
          rejectUnauthorized: this.strictSSL,
          uri: this.makeUri('/rapidviews/list', 'rest/greenhopper/'),
          method: 'GET',
          json: true
        };

        this.request(options, function(error, response) {

          if (error) {
              callback(error, null);
              return;
          }

          if (response.statusCode === 404) {
            callback('Invalid URL');
            return;
          }

          if (response.statusCode !== 200) {
            callback(response.statusCode + ': Unable to connect to JIRA during rapidView search.');
            return;
          }

          if (response.body !== null) {
            var rapidViews = response.body.views;
            for (var i = 0; i < rapidViews.length; i++) {
              if(rapidViews[i].name.toLowerCase() === projectName.toLowerCase()) {
                callback(null, rapidViews[i]);
                return;
              }
            }
          }
          
      });
    };

Get a list of Sprints belonging to a Rapid View

Takes

  • rapidViewId: the id for the rapid view
  • callback: for when it's done

Returns

  • error: string with the error
  • sprints: the ?array? of sprints
    /**
     * Returns a list of sprints belonging to a Rapid View.
     *
     * @param rapidView ID
     * @param callback
     */
    this.getLastSprintForRapidView = function(rapidViewId, callback) {

        var options = {
          rejectUnauthorized: this.strictSSL,
          uri: this.makeUri('/sprints/' + rapidViewId, 'rest/greenhopper/'),
          method: 'GET',
          json:true
        };

        this.request(options, function(error, response) {

          if (error) {
              callback(error, null);
              return;
          }
          
          if (response.statusCode === 404) {
            callback('Invalid URL');
            return;
          }

          if (response.statusCode !== 200) {
            callback(response.statusCode + ': Unable to connect to JIRA during sprints search.');
            return;
          }

          if (response.body !== null) {
            var sprints = response.body.sprints;
            callback(null, sprints.pop());
            return;
          }
          
        });
    };

Get the issues for a rapidView / sprint

Takes

  • rapidViewId: the id for the rapid view
  • sprintId: the id for the sprint
  • callback: for when it's done

Returns

  • error: string with the error
  • results: the object with the issues and additional sprint information
    /**
     * Returns sprint and issues information
     *
     * @param rapidView ID
     * @param sprint ID
     * @param callback
     */
    this.getSprintIssues = function getSprintIssues(rapidViewId, sprintId, callback) {

      var options = {
        rejectUnauthorized: this.strictSSL,
        uri: this.makeUri('/rapid/charts/sprintreport?rapidViewId=' + rapidViewId + '&sprintId=' + sprintId, 'rest/greenhopper/'),
        method: 'GET',
        json: true
      };

      this.request(options, function(error, response) {

        if (error) {
            callback(error, null);
            return;
        }
        
        if( response.statusCode === 404 ) {
          callback('Invalid URL');
          return;
        }

        if( response.statusCode !== 200 ) {
          callback(response.statusCode + ': Unable to connect to JIRA during sprints search');
          return;
        }

        if(response.body !== null) {
          callback(null, response.body);
        } else {
          callback('No body');
        }
        
      });

    };

Add an issue to the project's current sprint

Takes

  • issueId: the id of the existing issue
  • sprintId: the id of the sprint to add it to
  • callback: for when it's done

Returns

  • error: string of the error

does this callback if there's success?

    /**
     * Adds a given issue to a project's current sprint
     *
     * @param issueId
     * @param sprintId
     * @param callback
     */
    this.addIssueToSprint = function(issueId, sprintId, callback) {

        var options = {
          rejectUnauthorized: this.strictSSL,
          uri: this.makeUri('/sprint/' + sprintId + '/issues/add', 'rest/greenhopper/'),
          method: 'PUT',
          followAllRedirects: true,
          json:true,
          body: {
            issueKeys: [issueId]
          }
        };

        logger.log(options.uri);
        
        this.request(options, function(error, response) {

          if (error) {
              callback(error, null);
              return;
          }
          
          if (response.statusCode === 404) {
            callback('Invalid URL');
            return;
          }

          if (response.statusCode !== 204) {
            callback(response.statusCode + ': Unable to connect to JIRA to add to sprint.');
            return;
          }

        });
    };

Create an issue link between two issues

Takes

  • link: a link object
  • callback: for when it's done

Returns

  • error: string if there was an issue, null if success

Jira Doc

    /**
     * Creates an issue link between two issues. Link should follow the below format:
     *
     * {
     *   'linkType': 'Duplicate',
     *   'fromIssueKey': 'HSP-1',
     *   'toIssueKey': 'MKY-1',
     *   'comment': {
     *     'body': 'Linked related issue!',
     *     'visibility': {
     *       'type': 'GROUP',
     *       'value': 'jira-users'
     *     }
     *   }
     * }
     *
     * @param link
     * @param errorCallback
     * @param successCallback
     */
    this.issueLink = function(link, callback) {

        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issueLink'),
            method: 'POST',
            followAllRedirects: true,
            json: true,
            body: link
        };

        this.request(options, function(error, response) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 404) {
                callback('Invalid project.');
                return;
            }

            if (response.statusCode !== 200) {
                callback(response.statusCode + ': Unable to connect to JIRA during issueLink.');
                return;
            }

            callback(null);
            
        });
    };

Get Versions for a project

Takes

  • project: A project key
  • callback: for when it's done

Returns

  • error: a string with the error
  • versions: array of the versions for a product

Jira Doc

    this.getVersions = function(project, callback) {

        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/project/' + project + '/versions'),
            method: 'GET'
        };

        this.request(options, function(error, response, body) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 404) {
                callback('Invalid project.');
                return;
            }

            if (response.statusCode !== 200) {
                callback(response.statusCode + ': Unable to connect to JIRA during getVersions.');
                return;
            }

            body = JSON.parse(body);
            callback(null, body);
            
        });
    };

Create a version

Takes

  • version: an object of the new version
  • callback: for when it's done

Returns

  • error: error text
  • version: should be the same version you passed up

Jira Doc

    /* {
     *    "description": "An excellent version",
     *    "name": "New Version 1",
     *    "archived": false,
     *    "released": true,
     *    "releaseDate": "2010-07-05",
     *    "userReleaseDate": "5/Jul/2010",
     *    "project": "PXA"
     * }
     */
    this.createVersion = function(version, callback) {

        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/version'),
            method: 'POST',
            followAllRedirects: true,
            json: true,
            body: version
        };
        this.request(options, function(error, response, body) {
 
            if (error) {
                callback(error, null);
                return;
            }
 
            if (response.statusCode === 404) {
                callback('Version does not exist or the currently authenticated user does not have permission to view it');
                return;
            }

            if (response.statusCode === 403) {
                callback('The currently authenticated user does not have permission to edit the version');
                return;
            }

            if (response.statusCode !== 201) {
                callback(response.statusCode + ': Unable to connect to JIRA during createVersion.');
                return;
            }

            callback(null, body);
            
        });
    };
    

Pass a search query to Jira

Takes

  • searchString: jira query string
  • optional: object containing any of the following properties
    • startAt: optional index number (default 0)
    • maxResults: optional max results number (default 50)
    • fields: optional array of desired fields, defaults when null:
    • "summary"
    • "status"
    • "assignee"
    • "description"
  • callback: for when it's done

Returns

  • error: string if there's an error
  • issues: array of issues for the user

Jira Doc

    this.searchJira = function(searchString, optional, callback) {

backwards compatibility

        optional = optional || {};
        if (Array.isArray(optional)) {
            optional = { fields: optional };
        }

        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/search'),
            method: 'POST',
            json: true,
            followAllRedirects: true,
            body: {
                jql: searchString,
                startAt: optional.startAt || 0,
                maxResults: optional.maxResults || 50,
                fields: optional.fields || ["summary", "status", "assignee", "description"]
            }
        };

        this.request(options, function(error, response, body) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 400) {
                callback('Problem with the JQL query');
                return;
            }

            if (response.statusCode !== 200) {
                callback(response.statusCode + ': Unable to connect to JIRA during search.');
                return;
            }

            callback(null, body);
            
        });
    };
    

Get issues related to a user

Takes

  • user: username of user to search for
  • open: boolean determines if only open issues should be returned
  • callback: for when it's done

Returns

  • error: string if there's an error
  • issues: array of issues for the user

Jira Doc

    this.getUsersIssues = function(username, open, callback) {
        var jql = "assignee = " + username;
        var openText = ' AND status in (Open, "In Progress", Reopened)';
        if (open) { jql += openText; }
        this.searchJira(jql, {}, callback);
    };

Add issue to Jira

Takes

  • issue: Properly Formatted Issue
  • callback: for when it's done

Returns

  • error object (check out the Jira Doc)
  • success object

Jira Doc

    this.addNewIssue = function(issue, callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issue'),
            method: 'POST',
            followAllRedirects: true,
            json: true,
            body: issue
        };

        this.request(options, function(error, response, body) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 400) {
                callback(body);
                return;
            }

            if ((response.statusCode !== 200) && (response.statusCode !== 201)) {
                callback(response.statusCode + ': Unable to connect to JIRA during search.');
                return;
            }

            callback(null, body);
            
        });
    };

Delete issue to Jira

Takes

  • issueId: the Id of the issue to delete
  • callback: for when it's done

Returns

  • error string
  • success object

Jira Doc

    this.deleteIssue = function(issueNum, callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issue/' + issueNum),
            method: 'DELETE',
            followAllRedirects: true,
            json: true
        };

        this.request(options, function(error, response) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 204) {
                callback(null, "Success");
                return;
            }

            callback(response.statusCode + ': Error while deleting');
            
        });
    };

Update issue in Jira

Takes

  • issueId: the Id of the issue to delete
  • issueUpdate: update Object
  • callback: for when it's done

Returns

  • error string
  • success string

Jira Doc

    this.updateIssue = function(issueNum, issueUpdate, callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issue/' + issueNum),
            body: issueUpdate,
            method: 'PUT',
            followAllRedirects: true,
            json: true
        };

        this.request(options, function(error, response) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 200) {
                callback(null, "Success");
                return;
            }
            
            callback(response.statusCode + ': Error while updating');
            
        });
    };

List Transitions

Takes

  • issueId: get transitions available for the issue
  • callback: for when it's done

Returns

  • error string
  • array of transitions

Jira Doc

    /*
     *  {
     *  "expand": "transitions",
     *  "transitions": [
     *      {
     *          "id": "2",
     *          "name": "Close Issue",
     *          "to": {
     *              "self": "http://localhostname:8090/jira/rest/api/2.0/status/10000",
     *              "description": "The issue is currently being worked on.",
     *              "iconUrl": "http://localhostname:8090/jira/images/icons/progress.gif",
     *              "name": "In Progress",
     *              "id": "10000"
     *          },
     *          "fields": {
     *              "summary": {
     *                  "required": false,
     *                  "schema": {
     *                      "type": "array",
     *                      "items": "option",
     *                      "custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect",
     *                      "customId": 10001
     *                  },
     *                  "name": "My Multi Select",
     *                  "operations": [
     *                      "set",
     *                      "add"
     *                  ],
     *                  "allowedValues": [
     *                      "red",
     *                      "blue"
     *                  ]
     *              }
     *          }
     *      }
     *  ]}
     */
    this.listTransitions = function(issueId, callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issue/' + issueId + '/transitions'),
            method: 'GET',
            json: true
        };

        this.request(options, function(error, response, body) {

            if (error) {
                callback(error, null);
                return;
            }
            
            if (response.statusCode === 200) {
                callback(null, body.transitions);
                return;
            }
            if (response.statusCode === 404) {
                callback("Issue not found");
                return;
            }

            callback(response.statusCode + ': Error while updating');
            
        });
    };

Transition issue in Jira

Takes

  • issueId: the Id of the issue to delete
  • issueTransition: transition Object
  • callback: for when it's done

Returns

  • error string
  • success string

Jira Doc

    this.transitionIssue = function(issueNum, issueTransition, callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issue/' + issueNum + '/transitions'),
            body: issueTransition,
            method: 'POST',
            followAllRedirects: true,
            json: true
        };

        this.request(options, function(error, response) {

            if (error) {
                callback(error, null);
                return;
            }

            if (response.statusCode === 204) {
                callback(null, "Success");
                return;
            }
            
            callback(response.statusCode + ': Error while updating');
            
        });
    };
    

List all Viewable Projects

Takes

  • callback: for when it's done

Returns

  • error string
  • array of projects

Jira Doc

    /*
     * Result items are in the format:
     * {
     *      "self": "http://www.example.com/jira/rest/api/2/project/ABC",
     *      "id": "10001",
     *      "key": "ABC",
     *      "name": "Alphabetical",
     *      "avatarUrls": {
     *          "16x16": "http://www.example.com/jira/secure/projectavatar?size=small&pid=10001",
     *          "48x48": "http://www.example.com/jira/secure/projectavatar?size=large&pid=10001"
     *      }
     * }
     */
    this.listProjects = function(callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/project'),
            method: 'GET',
            json: true
        };

        this.request(options, function(error, response, body) {

            if (error) {
                callback(error, null);
                return;
            }
            
            if (response.statusCode === 200) {
                callback(null, body);
                return;
            }
            if (response.statusCode === 500) {
                callback(response.statusCode + ': Error while retrieving list.');
                return;
            }

            callback(response.statusCode + ': Error while updating');
            
        });
    };

Add a worklog to a project

Takes

  • issueId: Issue to add a worklog to
  • worklog: worklog object
  • callback: for when it's done

Returns

  • error string
  • success string

Jira Doc

    /*
     * Worklog item is in the format:
     *  {
     *      "self": "http://www.example.com/jira/rest/api/2.0/issue/10010/worklog/10000",
     *      "author": {
     *          "self": "http://www.example.com/jira/rest/api/2.0/user?username=fred",
     *          "name": "fred",
     *          "displayName": "Fred F. User",
     *          "active": false
     *      },
     *      "updateAuthor": {
     *          "self": "http://www.example.com/jira/rest/api/2.0/user?username=fred",
     *          "name": "fred",
     *          "displayName": "Fred F. User",
     *          "active": false
     *      },
     *      "comment": "I did some work here.",
     *      "visibility": {
     *          "type": "group",
     *          "value": "jira-developers"
     *      },
     *      "started": "2012-11-22T04:19:46.736-0600",
     *      "timeSpent": "3h 20m",
     *      "timeSpentSeconds": 12000,
     *      "id": "100028"
     *  }
     */
    this.addWorklog = function(issueId, worklog, callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issue/' + issueId + '/worklog'),
            body: worklog,
            method: 'POST',
            followAllRedirects: true,
            json: true
        };

        this.request(options, function(error, response, body) {
 
            if (error) {
                callback(error, null);
                return;
            }
 
            if (response.statusCode === 201) {
                callback(null, "Success");
                return;
            }
            if (response.statusCode === 400) {
                callback("Invalid Fields: " + JSON.stringify(body));
                return;
            }
            if (response.statusCode === 403) {
                callback("Insufficient Permissions");
                return;
            }
            
            callback(response.statusCode + ': Error while updating');
            
        });
    };

List all Issue Types

Takes

  • callback: for when it's done

Returns

  • error string
  • array of types

Jira Doc

    /*
     * Result items are in the format:
     * {
     *  "self": "http://localhostname:8090/jira/rest/api/2.0/issueType/3",
     *  "id": "3",
     *  "description": "A task that needs to be done.",
     *  "iconUrl": "http://localhostname:8090/jira/images/icons/task.gif",
     *  "name": "Task",
     *  "subtask": false
     * }
     */
    this.listIssueTypes = function(callback) {
        var options = {
            rejectUnauthorized: this.strictSSL,
            uri: this.makeUri('/issuetype'),
            method: 'GET',
            json: true
        };

        this.request(options, function(error, response, body) {

            if (error) {
                callback(error, null);
                return;
            }
            
            if (response.statusCode === 200) {
                callback(null, body);
                return;
            }
            
            callback(response.statusCode + ': Error while retrieving issue types');
            
        });
    };

}).call(JiraApi.prototype);