├── test ├── mocha.opts ├── .eslintrc ├── helper.js └── jira-tests.js ├── .eslintignore ├── .gitignore ├── .babelrc ├── .eslintrc ├── esdoc.json ├── .github └── workflows │ └── push.yml ├── .editorconfig ├── LICENSE.md ├── CONTRIBUTING.md ├── package.json ├── README.md └── src └── jira.js /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require @babel/register 2 | --recursive 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | docs/ 4 | tmp-docs-repo/ 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "expect": true 8 | }, 9 | "rules": { 10 | "no-unused-expressions": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | global.should = chai.should(); 5 | chai.use(chaiAsPromised); 6 | 7 | global.expect = chai.expect; 8 | global.assert = chai.assert; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | docs 14 | 15 | lib 16 | node_modules 17 | npm-debug.log 18 | 19 | .idea 20 | *.iml 21 | 22 | tmp-docs-repo 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "maintained node versions" 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "add-module-exports", 12 | "@babel/plugin-transform-runtime" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb/base" 4 | ], 5 | "env": { 6 | "node": true 7 | }, 8 | "parser": "@babel/eslint-parser", 9 | "rules": { 10 | "default-param-last": 0, 11 | "no-prototype-builtins": 0, 12 | "prefer-spread": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "excludes": ["node_modules"], 5 | "plugins": [ 6 | { 7 | "name": "esdoc-ecmascript-proposal-plugin", 8 | "option": { "objectRestSpread": true, "objectRestSpread": true } 9 | }, 10 | { "name": "esdoc-standard-plugin" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Run tests 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: ['12', '14', '16'] 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: ${{ matrix.node-version }} 14 | - run: npm ci 15 | - run: npm test 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | max_line_length = 100 16 | 17 | [{*.json,.travis.yml,.eslintrc,.babelrc}] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2011-2016 Steven Surowiec, Anson Wayman, and Matt Smith 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome community support with both pull requests and reporting bugs. Please 4 | don't hesitate to jump in. 5 | 6 | ## Review others' work 7 | 8 | Check out the list of outstanding pull requests if there is something you might 9 | be interested in. Maybe somebody is trying to fix that stupid bug that bothers 10 | you. Review the PR. Do you have any better ideas how to fix this problem? Let us 11 | know. 12 | 13 | Here is a helpful set of tips for making your own commits: https://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message 14 | 15 | ## Issues 16 | 17 | The issue tracker is the preferred channel for bug reports, features requests 18 | and submitting pull requests. 19 | 20 | _Note: Occasionally issues are opened that are unclear, or we cannot verify them. When the issue author has not responded to our questions for verification within 7 days then we will close the issue._ 21 | 22 | ## Tests 23 | 24 | All commits that fix bugs or add features need appropriate unit tests. 25 | 26 | ## Code Style 27 | 28 | Please adhere to the current code styling. We have included an `.editorconfig` 29 | at the repo's root to facilitate uniformity regardless of your editor. See the 30 | [editor config site][editorconfig] for integration details. 31 | 32 | We use [ESLint][eslint] for all JavaScript Linting. There should be no linting 33 | errors and no new warnings for new work. You are welcome to configure your 34 | editor to use ESLint or the `npm test` command will run unit tests and the 35 | linter. 36 | 37 | 38 | 39 | -[editorconfig]: http://editorconfig.org 40 | -[eslint]: http://eslint.org 41 | 42 | ## Visual Changes 43 | 44 | When making a visual change, if at all feasible please provide screenshots 45 | and/or screencasts of the proposed change. This will help us to understand the 46 | desired change easier. 47 | 48 | ## Docs 49 | 50 | Please update the docs with any API changes, the code and docs should always be 51 | in sync. 52 | 53 | ## Breaking changes 54 | 55 | We follow semantic versioning rather strictly. 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira-client", 3 | "version": "8.2.2", 4 | "description": "Wrapper for the JIRA API", 5 | "author": "Steven Surowiec ", 6 | "contributors": [ 7 | "Chris Moultrie ", 8 | "Lucas Vogelsang ", 9 | "Matt Smith ", 10 | "Anson Wayman " 11 | ], 12 | "homepage": "http://github.com/jira-node/node-jira-client", 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/jira-node/node-jira-client" 16 | }, 17 | "engine": { 18 | "node": ">=10.0.0" 19 | }, 20 | "main": "./lib/jira.js", 21 | "files": [ 22 | "lib", 23 | "docs", 24 | "LICENSE.md", 25 | "README.md" 26 | ], 27 | "license": "MIT", 28 | "dependencies": { 29 | "@babel/runtime": "^7.6.0", 30 | "postman-request": "^2.88.1-postman.30" 31 | }, 32 | "scripts": { 33 | "build": "rm -rf lib && babel src --out-dir lib", 34 | "docs-build": "rm -rf docs && esdoc -c esdoc.json", 35 | "lint": "eslint ./", 36 | "test": "npm run lint && mocha --require @babel/register && npm run docs-build", 37 | "prepublishOnly": "npm run build && npm run docs-build", 38 | "release": "release" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.6.0", 42 | "@babel/core": "^7.6.0", 43 | "@babel/eslint-parser": "7.16.3", 44 | "@babel/plugin-transform-runtime": "^7.6.0", 45 | "@babel/preset-env": "^7.6.0", 46 | "@babel/register": "^7.6.0", 47 | "babel-plugin-add-module-exports": "^1.0.0", 48 | "chai": "^4.2.0", 49 | "chai-as-promised": "^7.1.1", 50 | "esdoc": "^1.1.0", 51 | "esdoc-ecmascript-proposal-plugin": "^1.0.0", 52 | "esdoc-standard-plugin": "^1.0.0", 53 | "eslint": "^8.2.0", 54 | "eslint-config-airbnb": "^19.0.0", 55 | "eslint-plugin-import": "^2.18.2", 56 | "eslint-plugin-mocha": "^9.0.0", 57 | "mocha": "^9.1.3", 58 | "release-script": "^1.0.1", 59 | "rewire": "^6.0.0" 60 | }, 61 | "release-script": { 62 | "docsRepo": "git@github.com:jira-node/jira-node.github.io.git", 63 | "docsRoot": "docs/", 64 | "tmpDocsRepo": "tmp-docs-repo" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript JIRA API for node.js # 2 | 3 | A node.js module, which provides an object oriented wrapper for the Jira Rest API. 4 | 5 | [![Documentation](https://img.shields.io/badge/Documentation--green.svg)](https://jira-node.github.io/) 6 | [![Jira Rest API](https://img.shields.io/badge/Jira%20Rest%20API--green.svg)](http://docs.atlassian.com/jira/REST/latest/) 7 | [![Run tests](https://github.com/jira-node/node-jira-client/workflows/Run%20tests/badge.svg)](https://github.com/jira-node/node-jira-client/actions) 8 | [![npm](https://img.shields.io/npm/v/jira-client.svg)](https://www.npmjs.com/package/jira-client) 9 | [![Downloads](https://img.shields.io/npm/dm/jira-client.svg)](https://npmjs.com/jira-client) 10 | [![Install Size](https://packagephobia.now.sh/badge?p=jira-client)](https://packagephobia.now.sh/result?p=jira-client) 11 | [![dependency Status](https://david-dm.org/jira-node/node-jira-client/status.svg)](https://david-dm.org/jira-node/node-jira-client) 12 | [![devDependency Status](https://david-dm.org/jira-node/node-jira-client/dev-status.svg)](https://david-dm.org/jira-node/node-jira-client?type=dev) 13 | 14 | ## Installation ## 15 | 16 | Install with the node package manager [npm](http://npmjs.org): 17 | 18 | ```shell 19 | $ npm install jira-client 20 | ``` 21 | 22 | ## Examples ## 23 | 24 | ### Create the JIRA client ### 25 | 26 | ```javascript 27 | // With ES5 28 | var JiraApi = require('jira-client'); 29 | 30 | // With ES6 31 | import JiraApi from 'jira-client'; 32 | 33 | // Initialize 34 | var jira = new JiraApi({ 35 | protocol: 'https', 36 | host: 'jira.somehost.com', 37 | username: 'username', 38 | password: 'password', 39 | apiVersion: '2', 40 | strictSSL: true 41 | }); 42 | ``` 43 | 44 | ### Find the status of an issue ### 45 | 46 | ```javascript 47 | // ES5 48 | // We are using an ES5 Polyfill for Promise support. Please note that if you don't explicitly 49 | // apply a catch exceptions will get swallowed. Read up on ES6 Promises for further details. 50 | jira.findIssue(issueNumber) 51 | .then(function(issue) { 52 | console.log('Status: ' + issue.fields.status.name); 53 | }) 54 | .catch(function(err) { 55 | console.error(err); 56 | }); 57 | 58 | // ES6 59 | jira.findIssue(issueNumber) 60 | .then(issue => { 61 | console.log(`Status: ${issue.fields.status.name}`); 62 | }) 63 | .catch(err => { 64 | console.error(err); 65 | }); 66 | 67 | // ES7 68 | async function logIssueName() { 69 | try { 70 | const issue = await jira.findIssue(issueNumber); 71 | console.log(`Status: ${issue.fields.status.name}`); 72 | } catch (err) { 73 | console.error(err); 74 | } 75 | } 76 | 77 | ``` 78 | 79 | ## Documentation ## 80 | Can't find what you need in the readme? Check out our documentation here: https://jira-node.github.io/ 81 | -------------------------------------------------------------------------------- /test/jira-tests.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import rewire from 'rewire'; 3 | 4 | const JiraApi = rewire('../src/jira'); 5 | 6 | function getOptions(options) { 7 | const actualOptions = options || {}; 8 | return { 9 | protocol: actualOptions.protocol || 'http', 10 | host: actualOptions.host || 'jira.somehost.com', 11 | port: actualOptions.port || '8080', 12 | username: actualOptions.username || 'someusername', 13 | password: actualOptions.password || 'somepassword', 14 | apiVersion: actualOptions.apiVersion || '2.0', 15 | base: actualOptions.base || '', 16 | strictSSL: actualOptions.hasOwnProperty('strictSSL') ? actualOptions.strictSSL : true, 17 | request: actualOptions.request, 18 | oauth: actualOptions.oauth || null, 19 | intermediatePath: actualOptions.intermediatePath, 20 | bearer: actualOptions.bearer || null, 21 | ca: actualOptions.ca || null, 22 | }; 23 | } 24 | 25 | describe('Jira API Tests', () => { 26 | describe('Constructor Tests', () => { 27 | it('Constructor functions properly', () => { 28 | const jira = new JiraApi(getOptions()); 29 | 30 | expect(jira.protocol).to.eql('http'); 31 | expect(jira.host).to.eql('jira.somehost.com'); 32 | expect(jira.port).to.eql('8080'); 33 | expect(jira.baseOptions.auth.user).to.eql('someusername'); 34 | expect(jira.baseOptions.auth.pass).to.eql('somepassword'); 35 | expect(jira.apiVersion).to.eql('2.0'); 36 | }); 37 | 38 | it('Constructor with no auth credentials', () => { 39 | const { username, password, ...options } = getOptions(); 40 | 41 | expect(options.username).to.not.eql(username); 42 | expect(options.password).to.not.eql(password); 43 | 44 | const jira = new JiraApi(options); 45 | 46 | expect(jira.baseOptions.auth).to.be.undefined; 47 | }); 48 | 49 | it('Constructor with bearer credentials', () => { 50 | const options = getOptions({ 51 | bearer: 'testBearer', 52 | }); 53 | 54 | const jira = new JiraApi(options); 55 | 56 | expect(jira.baseOptions.auth).to.eql({ 57 | user: '', 58 | pass: '', 59 | sendImmediately: true, 60 | bearer: options.bearer, 61 | }); 62 | }); 63 | 64 | it('Constructor with oauth credentials', () => { 65 | const options = getOptions({ 66 | oauth: { 67 | consumer_key: 'consumer', 68 | consumer_secret: 'consumer_secret', 69 | access_token: 'token', 70 | access_token_secret: 'token_secret', 71 | }, 72 | }); 73 | 74 | const jira = new JiraApi(options); 75 | 76 | expect(jira.baseOptions.oauth).to.eql({ 77 | consumer_key: 'consumer', 78 | consumer_secret: 'consumer_secret', 79 | token: 'token', 80 | token_secret: 'token_secret', 81 | signature_method: 'RSA-SHA1', 82 | }); 83 | }); 84 | 85 | it('Constructor with timeout', () => { 86 | const jira = new JiraApi({ 87 | timeout: 2, 88 | ...getOptions(), 89 | }); 90 | 91 | expect(jira.baseOptions.timeout).to.equal(2); 92 | }); 93 | 94 | it('Constructor with strictSSL off', () => { 95 | const jira = new JiraApi( 96 | getOptions({ 97 | strictSSL: false, 98 | }), 99 | ); 100 | 101 | expect(jira.strictSSL).to.equal(false); 102 | }); 103 | 104 | it('should allow the user to pass in a certificate authority', () => { 105 | const jira = new JiraApi( 106 | getOptions({ 107 | ca: 'fakestring', 108 | }), 109 | ); 110 | 111 | expect(jira.baseOptions.ca).to.equal('fakestring'); 112 | }); 113 | }); 114 | 115 | describe('makeRequestHeader Tests', () => { 116 | it('makeRequestHeader functions properly in the average case', () => { 117 | const jira = new JiraApi(getOptions()); 118 | 119 | expect(jira.makeRequestHeader(jira.makeUri({ 120 | pathname: '/somePathName', 121 | }))).to.eql({ 122 | json: true, 123 | method: 'GET', 124 | rejectUnauthorized: true, 125 | uri: 'http://jira.somehost.com:8080/rest/api/2.0/somePathName', 126 | }); 127 | }); 128 | 129 | it('makeRequestHeader functions properly with a different method', () => { 130 | const jira = new JiraApi(getOptions()); 131 | 132 | expect(jira.makeRequestHeader(jira.makeUri({ 133 | pathname: '/somePathName', 134 | }), { method: 'POST' })).to.eql({ 135 | json: true, 136 | method: 'POST', 137 | rejectUnauthorized: true, 138 | uri: 'http://jira.somehost.com:8080/rest/api/2.0/somePathName', 139 | }); 140 | }); 141 | }); 142 | 143 | describe('makeUri', () => { 144 | it('builds url with pathname and default host, protocol, port, and base api', () => { 145 | const jira = new JiraApi(getOptions()); 146 | 147 | expect(jira.makeUri({ pathname: '/somePathName' })) 148 | .to.eql('http://jira.somehost.com:8080/rest/api/2.0/somePathName'); 149 | }); 150 | 151 | it('builds url with intermediatePath', () => { 152 | const jira = new JiraApi(getOptions()); 153 | 154 | expect(jira.makeUri({ pathname: '/somePathName', intermediatePath: 'intermediatePath' })) 155 | .to.eql('http://jira.somehost.com:8080/intermediatePath/somePathName'); 156 | }); 157 | 158 | it('builds url with globally specified intermediatePath', () => { 159 | const jira = new JiraApi(getOptions({ 160 | intermediatePath: 'intermediatePath', 161 | })); 162 | 163 | expect(jira.makeUri({ pathname: '/somePathName' })) 164 | .to.eql('http://jira.somehost.com:8080/intermediatePath/somePathName'); 165 | }); 166 | 167 | it('builds url with query string parameters', () => { 168 | const jira = new JiraApi(getOptions()); 169 | 170 | const url = jira.makeUri({ 171 | pathname: '/path', 172 | query: { 173 | fields: [ 174 | 'one', 175 | 'two', 176 | ], 177 | expand: 'three', 178 | }, 179 | }); 180 | 181 | url.should.eql( 182 | 'http://jira.somehost.com:8080/rest/api/2.0/path?fields=one&fields=two&expand=three', 183 | ); 184 | }); 185 | 186 | it('makeWebhookUri functions properly in the average case', () => { 187 | const jira = new JiraApi(getOptions()); 188 | 189 | expect(jira.makeWebhookUri({ 190 | pathname: '/somePathName', 191 | })) 192 | .to.eql('http://jira.somehost.com:8080/rest/webhooks/1.0/somePathName'); 193 | }); 194 | 195 | it('makeWebhookUri functions with intermediate path', () => { 196 | const jira = new JiraApi(getOptions()); 197 | 198 | expect(jira.makeWebhookUri({ 199 | pathname: '/somePathName', 200 | intermediatePath: '/someIntermediatePath', 201 | })) 202 | .to.eql('http://jira.somehost.com:8080/someIntermediatePath/somePathName'); 203 | }); 204 | 205 | it('makeSprintQueryUri functions properly in the average case', () => { 206 | const jira = new JiraApi(getOptions()); 207 | 208 | expect(jira.makeSprintQueryUri({ 209 | pathname: '/somePathName', 210 | })) 211 | .to.eql('http://jira.somehost.com:8080/rest/greenhopper/1.0/somePathName'); 212 | }); 213 | 214 | it('makeSprintQueryUri functions properly in the average case', () => { 215 | const jira = new JiraApi(getOptions()); 216 | 217 | expect(jira.makeSprintQueryUri({ 218 | pathname: '/somePathName', 219 | intermediatePath: '/someIntermediatePath', 220 | })) 221 | .to.eql('http://jira.somehost.com:8080/someIntermediatePath/somePathName'); 222 | }); 223 | 224 | it('makeUri functions properly no port http', () => { 225 | const { port, ...options } = getOptions(); 226 | 227 | expect(options.port).to.not.eql(port); 228 | 229 | const jira = new JiraApi(options); 230 | 231 | expect(jira.makeUri({ 232 | pathname: '/somePathName', 233 | })) 234 | .to.eql('http://jira.somehost.com/rest/api/2.0/somePathName'); 235 | }); 236 | 237 | it('makeUri functions properly no port https', () => { 238 | const { port, ...options } = getOptions({ protocol: 'https' }); 239 | 240 | expect(options.port).to.not.eql(port); 241 | 242 | const jira = new JiraApi(options); 243 | 244 | expect(jira.makeUri({ 245 | pathname: '/somePathName', 246 | })) 247 | .to.eql('https://jira.somehost.com/rest/api/2.0/somePathName'); 248 | }); 249 | }); 250 | 251 | describe('doRequest Tests', () => { 252 | it('doRequest functions properly in the average case', async () => { 253 | // eslint-disable-next-line no-underscore-dangle 254 | const revert = JiraApi.__set__('_request', (uri, options, callback) => { 255 | callback(undefined, { body: 'Successful response!' }); 256 | }); 257 | 258 | const jira = new JiraApi(getOptions()); 259 | 260 | const response = await jira.doRequest({}); 261 | response.should.eql('Successful response!'); 262 | 263 | revert(); 264 | }); 265 | 266 | it('doRequest authenticates properly when specified', async () => { 267 | async function dummyRequest(requestOptions) { 268 | return requestOptions; 269 | } 270 | 271 | const username = 'someusername'; 272 | const password = 'somepassword'; 273 | 274 | const jira = new JiraApi(getOptions({ 275 | username, 276 | password, 277 | request: dummyRequest, 278 | })); 279 | 280 | const result = await jira.doRequest({}); 281 | expect(result.auth.user).to.eql(username); 282 | expect(result.auth.pass).to.eql(password); 283 | }); 284 | 285 | it('doRequest times out with specified option', async () => { 286 | async function dummyRequest(requestOptions) { 287 | return requestOptions; 288 | } 289 | 290 | const jira = new JiraApi({ 291 | timeout: 2, 292 | ...getOptions({ request: dummyRequest }), 293 | }); 294 | 295 | const result = await jira.doRequest({}); 296 | expect(result.timeout).to.eql(2); 297 | }); 298 | 299 | it('doRequest throws an error properly', async () => { 300 | // eslint-disable-next-line no-underscore-dangle 301 | const revert = JiraApi.__set__('_request', (uri, options, callback) => { 302 | callback({ 303 | body: JSON.stringify({ 304 | errorMessages: ['some error to throw'], 305 | }), 306 | }); 307 | }); 308 | 309 | const jira = new JiraApi(getOptions()); 310 | 311 | await jira.doRequest({}) 312 | .should.eventually.be.rejectedWith('{"body":"{\\"errorMessages\\":[\\"some error to throw\\"]}"}'); 313 | 314 | revert(); 315 | }); 316 | 317 | it('doRequest throws a list of errors properly', async () => { 318 | // eslint-disable-next-line no-underscore-dangle 319 | const revert = JiraApi.__set__('_request', (uri, options, callback) => { 320 | callback({ 321 | body: 322 | JSON.stringify({ errorMessages: ['some error to throw', 'another error'] }), 323 | }); 324 | }); 325 | 326 | const jira = new JiraApi(getOptions()); 327 | 328 | await jira.doRequest({}) 329 | .should.eventually.be.rejectedWith('{"body":"{\\"errorMessages\\":[\\"some error to throw\\",\\"another error\\"]}"}'); 330 | 331 | revert(); 332 | }); 333 | 334 | it('doRequest does not throw an error on empty response', async () => { 335 | // eslint-disable-next-line no-underscore-dangle 336 | const revert = JiraApi.__set__('_request', (uri, options, callback) => { 337 | callback(undefined, { 338 | body: undefined, 339 | }); 340 | }); 341 | 342 | const jira = new JiraApi(getOptions()); 343 | 344 | const response = await jira.doRequest({}); 345 | expect(response).to.be.undefined; 346 | 347 | revert(); 348 | }); 349 | 350 | it('doRequest throws an error when request failed', async () => { 351 | // eslint-disable-next-line no-underscore-dangle 352 | const revert = JiraApi.__set__('_request', (uri, options, callback) => { 353 | callback('This is an error message', undefined); 354 | }); 355 | 356 | const jira = new JiraApi(getOptions()); 357 | 358 | await jira.doRequest({}) 359 | .should.eventually.be.rejectedWith('This is an error message'); 360 | 361 | revert(); 362 | }); 363 | }); 364 | 365 | describe('Request Functions Tests', () => { 366 | async function dummyURLCall(jiraApiMethodName, functionArguments, dummyRequestMethod, returnedValue = 'uri') { 367 | let dummyRequest = dummyRequestMethod; 368 | if (!dummyRequest) { 369 | dummyRequest = async (requestOptions) => requestOptions; 370 | } 371 | 372 | const jira = new JiraApi( 373 | getOptions({ 374 | request: dummyRequest, 375 | }), 376 | ); 377 | 378 | const resultObject = await jira[jiraApiMethodName].apply(jira, functionArguments); 379 | 380 | // hack exposing the qs object as the query string in the URL so this is 381 | // uniformly testable 382 | if (resultObject.qs) { 383 | const queryString = Object.keys(resultObject.qs).map((x) => `${x}=${resultObject.qs[x]}`) 384 | .join('&'); 385 | return `${resultObject.uri}?${queryString}`; 386 | } 387 | 388 | return resultObject[returnedValue]; 389 | } 390 | 391 | it('findIssue hits proper url', async () => { 392 | const result = await dummyURLCall('findIssue', ['PK-100']); 393 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/PK-100?expand=&fields=*all&properties=*all&fieldsByKeys=false'); 394 | }); 395 | 396 | it('findIssue hits proper url with expansion', async () => { 397 | const result = await dummyURLCall('findIssue', ['PK-100', 'transitions,changelog']); 398 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/PK-100?expand=transitions,changelog&fields=*all&properties=*all&fieldsByKeys=false'); 399 | }); 400 | 401 | it('findIssue hits proper url with fields', async () => { 402 | const result = await dummyURLCall('findIssue', ['PK-100', null, 'transitions,changelog']); 403 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/PK-100?expand=&fields=transitions,changelog&properties=*all&fieldsByKeys=false'); 404 | }); 405 | 406 | it('findIssue hits proper url with properties', async () => { 407 | const result = await dummyURLCall('findIssue', ['PK-100', null, null, 'transitions,changelog']); 408 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/PK-100?expand=&fields=*all&properties=transitions,changelog&fieldsByKeys=false'); 409 | }); 410 | 411 | it('findIssue hits proper url with fields and fieldsByKeys', async () => { 412 | const result = await dummyURLCall('findIssue', ['PK-100', null, 'transitions,changelog', null, true]); 413 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/PK-100?expand=&fields=transitions,changelog&properties=*all&fieldsByKeys=true'); 414 | }); 415 | 416 | it('downloadAttachment hits proper url with attachment id and filename', async () => { 417 | const result = await dummyURLCall('downloadAttachment', [{ id: '123456', filename: 'attachment.txt' }]); 418 | result.should.eql('http://jira.somehost.com:8080/secure/attachment/123456/attachment.txt'); 419 | }); 420 | 421 | it('downloadAttachment hits proper url with attachment id and filename with special characters', async () => { 422 | const result = await dummyURLCall('downloadAttachment', [{ id: '123456', filename: 'attachment-æøå.txt' }]); 423 | result.should.eql('http://jira.somehost.com:8080/secure/attachment/123456/attachment-%C3%A6%C3%B8%C3%A5.txt'); 424 | }); 425 | 426 | it('deleteAttachment hits proper url', async () => { 427 | const result = await dummyURLCall('deleteAttachment', ['123456']); 428 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/attachment/123456'); 429 | }); 430 | 431 | it('getUnresolvedIssueCount hits proper url', async () => { 432 | async function dummyRequest(requestOptions) { 433 | return { issuesUnresolvedCount: requestOptions }; 434 | } 435 | 436 | const result = await dummyURLCall('getUnresolvedIssueCount', ['someVersion'], dummyRequest); 437 | result.should 438 | .eql('http://jira.somehost.com:8080/rest/api/2.0/version/someVersion/unresolvedIssueCount'); 439 | }); 440 | 441 | it('getProject hits proper url', async () => { 442 | const result = await dummyURLCall('getProject', ['someProject']); 443 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/project/someProject'); 444 | }); 445 | 446 | it('createProject hits proper url', async () => { 447 | const result = await dummyURLCall('createProject', ['dummyObject']); 448 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/project/'); 449 | }); 450 | 451 | it('findRapidView hits proper url', async () => { 452 | async function dummyRequest(requestOptions) { 453 | return { 454 | views: [{ 455 | ...requestOptions, 456 | name: 'theNameToLookFor', 457 | }], 458 | }; 459 | } 460 | 461 | const result = await dummyURLCall('findRapidView', ['theNameToLookFor'], dummyRequest); 462 | result.should.eql('http://jira.somehost.com:8080/rest/greenhopper/1.0/rapidviews/list'); 463 | }); 464 | 465 | it('getLastSprintForRapidView hits proper url', async () => { 466 | async function dummyRequest(requestOptions) { 467 | return { 468 | sprints: [requestOptions], 469 | }; 470 | } 471 | 472 | const result = await dummyURLCall( 473 | 'getLastSprintForRapidView', 474 | ['someRapidViewId'], 475 | dummyRequest, 476 | ); 477 | 478 | result.should.eql( 479 | 'http://jira.somehost.com:8080/rest/greenhopper/1.0/sprintquery/someRapidViewId', 480 | ); 481 | }); 482 | 483 | it('getSprintIssues hits proper url', async () => { 484 | const result = await dummyURLCall('getSprintIssues', ['someRapidView', 'someSprintId']); 485 | result.should 486 | .eql('http://jira.somehost.com:8080/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=someRapidView&sprintId=someSprintId'); 487 | }); 488 | 489 | it('listSprints hits proper url', async () => { 490 | const result = await dummyURLCall('listSprints', ['someRapidViewId']); 491 | result.should.eql( 492 | 'http://jira.somehost.com:8080/rest/greenhopper/1.0/sprintquery/someRapidViewId', 493 | ); 494 | }); 495 | 496 | it('addIssueToSprint hits proper url', async () => { 497 | const result = await dummyURLCall('addIssueToSprint', ['someIssueId', 'someSprintId']); 498 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/sprint/someSprintId/issue'); 499 | }); 500 | 501 | it('issueLink hits proper url', async () => { 502 | const result = await dummyURLCall('issueLink', ['somelink']); 503 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issueLink'); 504 | }); 505 | 506 | it('getRemoteLinks hits proper url', async () => { 507 | const result = await dummyURLCall('getRemoteLinks', ['someIssueId']); 508 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueId/remotelink'); 509 | }); 510 | 511 | it('createRemoteLink hits proper url', async () => { 512 | const result = await dummyURLCall('createRemoteLink', ['issueNumber', 'someRemoteLink']); 513 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/issueNumber/remotelink'); 514 | }); 515 | 516 | it('getVersion hits proper url', async () => { 517 | const result = await dummyURLCall('getVersion', ['someVersion']); 518 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/version/someVersion'); 519 | }); 520 | 521 | it('getVersions hits proper url', async () => { 522 | const result = await dummyURLCall('getVersions', ['someProject']); 523 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/project/someProject/versions'); 524 | }); 525 | 526 | it('getVersions hits proper url with query', async () => { 527 | const result = await dummyURLCall('getVersions', ['someProject', { maxResults: 10 }]); 528 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/project/someProject/versions?maxResults=10'); 529 | }); 530 | 531 | it('createVersion hits proper url', async () => { 532 | const result = await dummyURLCall('createVersion', ['someVersion']); 533 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/version'); 534 | }); 535 | 536 | it('updateVersion hits proper url', async () => { 537 | const result = await dummyURLCall('updateVersion', [{ id: 'someVersionId' }]); 538 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/version/someVersionId'); 539 | }); 540 | 541 | it('deleteVersion hits proper url', async () => { 542 | const result = await dummyURLCall('deleteVersion', [ 543 | 'someVersionId', 544 | 'someFixVersionId', 545 | 'someAffectedVersionId', 546 | ]); 547 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/version/someVersionId?moveFixIssuesTo=someFixVersionId&moveAffectedIssuesTo=someAffectedVersionId'); 548 | }); 549 | 550 | it('seachJira hits proper url', async () => { 551 | const result = await dummyURLCall('searchJira', ['someJQLhere', 'someOptionsObject']); 552 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/search'); 553 | }); 554 | 555 | it('createUser hits proper url', async () => { 556 | const result = await dummyURLCall('createUser', [{ 557 | name: 'someUsername', 558 | emailAddress: 'someEmail', 559 | displayName: 'Some Name', 560 | }]); 561 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/user'); 562 | }); 563 | 564 | it('searchUsers hits proper url', async () => { 565 | const result = await dummyURLCall('searchUsers', [{ 566 | query: 'someOtherUserName', 567 | username: 'someUserName', 568 | }]); 569 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/user/search?username=someUserName&query=someOtherUserName&startAt=0&maxResults=50&includeActive=true&includeInactive=false'); 570 | }); 571 | 572 | it('getUsersInGroup hits proper url', async () => { 573 | const result = await dummyURLCall('getUsersInGroup', ['someGroupName']); 574 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/group?groupname=someGroupName&expand=users[0:50]'); 575 | }); 576 | 577 | it('getMembersOfGroup hits proper url', async () => { 578 | const result = await dummyURLCall('getMembersOfGroup', ['someGroupName']); 579 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/group/member?groupname=someGroupName&expand=users[0:50]&includeInactiveUsers=false'); 580 | }); 581 | 582 | it('getUsersIssues hits proper url', async () => { 583 | const result = await dummyURLCall('getUsersIssues', ['someUsername', true]); 584 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/search'); 585 | }); 586 | 587 | it('getUser hits proper url', async () => { 588 | const result = await dummyURLCall('getUser', ['some-account-Id', 'groups,applicationRoles']); 589 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/user?accountId=some-account-Id&expand=groups,applicationRoles'); 590 | }); 591 | 592 | it('getUsers hits proper url', async () => { 593 | const result = await dummyURLCall('getUsers', [0, 50]); 594 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/users?startAt=0&maxResults=50'); 595 | }); 596 | 597 | it('addNewIssue hits proper url', async () => { 598 | const result = await dummyURLCall('addNewIssue', ['someIssue']); 599 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue'); 600 | }); 601 | 602 | it('getUsersIssues hits proper url', async () => { 603 | const result = await dummyURLCall('getUsersIssues', ['someUsername', 'true']); 604 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/search'); 605 | }); 606 | 607 | it('addWatcher hits proper url', async () => { 608 | const result = await dummyURLCall('addWatcher', ['ZQ-9001']); 609 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/ZQ-9001/watchers'); 610 | }); 611 | 612 | it('addWatcher sends unquoted string in body', async () => { 613 | const result = await dummyURLCall('addWatcher', ['ZQ-9001', 'John Smith'], null, 'body'); 614 | result.should.eql('John Smith'); 615 | }); 616 | 617 | it('getIssueChangelog hits proper url', async () => { 618 | const result = await dummyURLCall('getIssueChangelog', ['ZQ-9001']); 619 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/ZQ-9001/changelog?startAt=0&maxResults=50'); 620 | }); 621 | 622 | it('getIssueWatchers hits proper url', async () => { 623 | const result = await dummyURLCall('getIssueWatchers', ['ZQ-9001']); 624 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/ZQ-9001/watchers'); 625 | }); 626 | 627 | it('updateAssignee hits proper url', async () => { 628 | const result = await dummyURLCall('updateAssignee', ['ZQ-9001', 'Assignee']); 629 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/ZQ-9001/assignee'); 630 | }); 631 | 632 | it('updateAssigneeWithId hits proper url', async () => { 633 | const result = await dummyURLCall('updateAssigneeWithId', ['ZQ-9001', 'Assignee']); 634 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/ZQ-9001/assignee'); 635 | }); 636 | 637 | it('deleteIssue hits proper url', async () => { 638 | const result = await dummyURLCall('deleteIssue', ['FU-69']); 639 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/FU-69'); 640 | }); 641 | 642 | it('updateIssue hits proper url', async () => { 643 | const result = await dummyURLCall('updateIssue', ['MI-6', 'someInfo']); 644 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/MI-6'); 645 | }); 646 | 647 | it('properly creates query params for updateIssue', async () => { 648 | const result = await dummyURLCall('updateIssue', ['MI-6', 'someInfo', { notifyUsers: true }]); 649 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/MI-6?notifyUsers=true'); 650 | }); 651 | 652 | it('listComponents hits proper url', async () => { 653 | const result = await dummyURLCall('listComponents', ['ProjectName']); 654 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/project/ProjectName/components'); 655 | }); 656 | 657 | it('addNewComponent hits proper url', async () => { 658 | const result = await dummyURLCall('addNewComponent', ['someComponent']); 659 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/component'); 660 | }); 661 | 662 | it('updateComponent hits proper url', async () => { 663 | const result = await dummyURLCall('updateComponent', ['someComponentNumber', 'someComponent']); 664 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/component/someComponentNumber'); 665 | }); 666 | 667 | it('deleteComponent hits proper url', async () => { 668 | const result = await dummyURLCall('deleteComponent', ['someComponentNumber']); 669 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/component/someComponentNumber'); 670 | }); 671 | 672 | it('deleteComponent hits proper url', async () => { 673 | const result = await dummyURLCall('deleteComponent', ['someComponentNumber', 'someComponentName']); 674 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/component/someComponentNumber?moveIssuesTo=someComponentName'); 675 | }); 676 | 677 | it('relatedIssueCounts hits proper url', async () => { 678 | const result = await dummyURLCall('relatedIssueCounts', ['someComponentNumber', 'someComponentName']); 679 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/component/someComponentNumber/relatedIssueCounts'); 680 | }); 681 | 682 | // Field APIs Suite Tests 683 | describe('Field APIs Suite Tests', () => { 684 | it('createCustomField hits proper url', async () => { 685 | const result = await dummyURLCall('createCustomField', ['someField']); 686 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field'); 687 | }); 688 | 689 | it('listFields hits proper url', async () => { 690 | const result = await dummyURLCall('listFields', []); 691 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field'); 692 | }); 693 | }); 694 | 695 | // Field Option APIs Suite Tests 696 | describe('Field Option APIs Suite Tests', () => { 697 | it('createFieldOption hits proper url', async () => { 698 | const result = await dummyURLCall('createFieldOption', ['someFieldKey', 'someOption']); 699 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field/someFieldKey/option'); 700 | }); 701 | 702 | it('listFieldOptions hits proper url', async () => { 703 | const result = await dummyURLCall('listFieldOptions', ['someFieldKey']); 704 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field/someFieldKey/option'); 705 | }); 706 | 707 | it('upsertFieldOption hits proper url', async () => { 708 | const result = await dummyURLCall('upsertFieldOption', ['someFieldKey', 'someOptionId', 'someOption']); 709 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field/someFieldKey/option/someOptionId'); 710 | }); 711 | 712 | it('getFieldOption hits proper url', async () => { 713 | const result = await dummyURLCall('getFieldOption', ['someFieldKey', 'someOptionId']); 714 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field/someFieldKey/option/someOptionId'); 715 | }); 716 | 717 | it('deleteFieldOption hits proper url', async () => { 718 | const result = await dummyURLCall('deleteFieldOption', ['someFieldKey', 'someOptionId']); 719 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field/someFieldKey/option/someOptionId'); 720 | }); 721 | }); 722 | 723 | it('getIssueProperty hits proper url with expansion', async () => { 724 | const result = await dummyURLCall('getIssueProperty', ['PK-100', 'somePropertyKey']); 725 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/PK-100/properties/somePropertyKey'); 726 | }); 727 | 728 | it('listPriorities hits proper url', async () => { 729 | const result = await dummyURLCall('listPriorities', []); 730 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/priority'); 731 | }); 732 | 733 | it('listTransitions hits proper url', async () => { 734 | const result = await dummyURLCall('listTransitions', ['someIssueNumber']); 735 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/transitions?expand=transitions.fields'); 736 | }); 737 | 738 | it('transitionIssue hits proper url', async () => { 739 | const result = await dummyURLCall('transitionIssue', [ 740 | 'someIssueNumber', 741 | 'someTransitionObject', 742 | ]); 743 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/transitions'); 744 | }); 745 | 746 | it('listProjects hits proper url', async () => { 747 | const result = await dummyURLCall('listProjects', []); 748 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/project'); 749 | }); 750 | 751 | it('addComment hits proper url', async () => { 752 | const result = await dummyURLCall('addComment', ['someIssueNumber', 'someComment']); 753 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/comment'); 754 | }); 755 | 756 | it('addCommentAdvanced hits proper url', async () => { 757 | const result = await dummyURLCall('addCommentAdvanced', ['someIssueNumber', 'someComment']); 758 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/comment'); 759 | }); 760 | 761 | it('updateComment hits proper url', async () => { 762 | const result = await dummyURLCall('updateComment', ['someIssueNumber', 'someCommentNumber', 'someComment']); 763 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/comment/someCommentNumber'); 764 | }); 765 | 766 | it('getComments hits proper url', async () => { 767 | const result = await dummyURLCall('getComments', ['someIssueNumber']); 768 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/comment'); 769 | }); 770 | 771 | it('getComment hits proper url', async () => { 772 | const result = await dummyURLCall('getComment', ['someIssueNumber', 'someCommentNumber']); 773 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/comment/someCommentNumber'); 774 | }); 775 | 776 | it('deleteComment hits proper url', async () => { 777 | const result = await dummyURLCall('deleteComment', ['someIssueNumber', 'someCommentNumber']); 778 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/comment/someCommentNumber'); 779 | }); 780 | 781 | it('addWorklog hits proper url', async () => { 782 | const result = await dummyURLCall('addWorklog', [ 783 | 'someIssueNumber', 784 | 'someWorkLog', 785 | 'someNewEstimate', 786 | ]); 787 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/worklog?adjustEstimate=new&newEstimate=someNewEstimate'); 788 | }); 789 | 790 | it('addWorklog hits proper url with adjustEstimate=leave', async () => { 791 | const result = await dummyURLCall('addWorklog', [ 792 | 'someIssueNumber', 793 | 'someWorkLog', 794 | '', 795 | { adjustEstimate: 'leave' }, 796 | ]); 797 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/worklog?adjustEstimate=leave'); 798 | }); 799 | 800 | it('deleteWorklog hits proper url', async () => { 801 | const result = await dummyURLCall('deleteWorklog', ['someIssueNumber', 'someWorklogId']); 802 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/worklog/someWorklogId'); 803 | }); 804 | 805 | it('updateWorklog hits proper url', async () => { 806 | const result = await dummyURLCall('updateWorklog', ['someIssueNumber', 'someWorklogId']); 807 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/worklog/someWorklogId'); 808 | }); 809 | 810 | it('deleteIssueLink hits proper url', async () => { 811 | const result = await dummyURLCall('deleteIssueLink', ['someLinkId']); 812 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issueLink/someLinkId'); 813 | }); 814 | 815 | it('getWorklogs hits proper url', async () => { 816 | const result = await dummyURLCall('getWorklogs'); 817 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/worklog/list?expand='); 818 | }); 819 | 820 | it('getIssueWorklogs hits proper url', async () => { 821 | const result = await dummyURLCall('getIssueWorklogs', ['someIssueNumber']); 822 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueNumber/worklog?startAt=0&maxResults=1000'); 823 | }); 824 | 825 | it('listIssueTypes hits proper url', async () => { 826 | const result = await dummyURLCall('listIssueTypes', []); 827 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issuetype'); 828 | }); 829 | 830 | it('listIssueLinkTypes hits proper url', async () => { 831 | const result = await dummyURLCall('listIssueLinkTypes', []); 832 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issueLinkType'); 833 | }); 834 | 835 | it('registerWebhook hits proper url', async () => { 836 | const result = await dummyURLCall('registerWebhook', ['someWebhook']); 837 | result.should.eql('http://jira.somehost.com:8080/rest/webhooks/1.0/webhook'); 838 | }); 839 | 840 | it('listWebhooks hits proper url', async () => { 841 | const result = await dummyURLCall('listWebhooks', []); 842 | result.should.eql('http://jira.somehost.com:8080/rest/webhooks/1.0/webhook'); 843 | }); 844 | 845 | it('getWebhook hits proper url', async () => { 846 | const result = await dummyURLCall('getWebhook', ['someWebhookId']); 847 | result.should.eql('http://jira.somehost.com:8080/rest/webhooks/1.0/webhook/someWebhookId'); 848 | }); 849 | 850 | it('deleteWebhook hits proper url', async () => { 851 | const result = await dummyURLCall('deleteWebhook', ['someWebhookId']); 852 | result.should.eql('http://jira.somehost.com:8080/rest/webhooks/1.0/webhook/someWebhookId'); 853 | }); 854 | 855 | it('getCurrentUser hits proper url', async () => { 856 | const result = await dummyURLCall('getCurrentUser', []); 857 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/myself'); 858 | }); 859 | 860 | it('getBacklogForRapidView hits proper url', async () => { 861 | const result = await dummyURLCall('getBacklogForRapidView', ['someRapidViewId']); 862 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/xboard/plan/backlog/data?rapidViewId=someRapidViewId'); 863 | }); 864 | 865 | it('addAttachmentOnIssue hits proper url', async () => { 866 | const result = await dummyURLCall('addAttachmentOnIssue', ['someIssueId', {}]); 867 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueId/attachments'); 868 | }); 869 | 870 | it('addAttachmentOnIssue hits proper url', async () => { 871 | const result = await dummyURLCall('listStatus'); 872 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/status'); 873 | }); 874 | 875 | // Field Option APIs Suite Tests 876 | describe('Dev-Status APIs Suite Tests', () => { 877 | it('getDevStatusSummary hits proper url', async () => { 878 | const result = await dummyURLCall('getDevStatusSummary', ['someIssueId']); 879 | result.should.eql('http://jira.somehost.com:8080/rest/dev-status/latest/issue/summary?issueId=someIssueId'); 880 | }); 881 | 882 | it('getDevStatusDetail hits proper url - repo', async () => { 883 | const result = await dummyURLCall('getDevStatusDetail', ['someIssueId', 'someApplicationType', 'repository']); 884 | result.should.eql('http://jira.somehost.com:8080/rest/dev-status/latest/issue/detail?issueId=someIssueId&applicationType=someApplicationType&dataType=repository'); 885 | }); 886 | 887 | it('getDevStatusDetail hits proper url - pullrequest', async () => { 888 | const result = await dummyURLCall('getDevStatusDetail', ['someIssueId', 'someApplicationType', 'pullrequest']); 889 | result.should.eql('http://jira.somehost.com:8080/rest/dev-status/latest/issue/detail?issueId=someIssueId&applicationType=someApplicationType&dataType=pullrequest'); 890 | }); 891 | }); 892 | 893 | // Agile APIs Suite Tests 894 | describe('Agile APIs Suite Tests', () => { 895 | it('getIssue hits proper url', async () => { 896 | const result = await dummyURLCall('getIssue', ['someIssueId']); 897 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/issue/someIssueId?fields=&expand='); 898 | }); 899 | 900 | it('getIssueEstimationForBoard hits proper url', async () => { 901 | const result = await dummyURLCall('getIssueEstimationForBoard', ['someIssueId', 'someBoardId']); 902 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/issue/someIssueId/estimation?boardId=someBoardId'); 903 | }); 904 | 905 | it('estimateIssueForBoard hits proper url', async () => { 906 | const result = await dummyURLCall('estimateIssueForBoard', ['someIssueId', 'someBoardId']); 907 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/issue/someIssueId/estimation?boardId=someBoardId'); 908 | }); 909 | 910 | it('rankIssues hits proper url', async () => { 911 | const result = await dummyURLCall('rankIssues'); 912 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/issue/rank'); 913 | }); 914 | 915 | it('moveToBacklog hits proper url', async () => { 916 | const result = await dummyURLCall('moveToBacklog'); 917 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/backlog/issue'); 918 | }); 919 | 920 | it('getAllBoards hits proper url', async () => { 921 | const result = await dummyURLCall('getAllBoards'); 922 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board?startAt=0&maxResults=50&type=&name='); 923 | }); 924 | 925 | it('getAllBoards hits proper url with project key provided', async () => { 926 | const result = await dummyURLCall('getAllBoards', [0, 50, undefined, undefined, 'someProjectKey']); 927 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board?startAt=0&maxResults=50&type=&name=&projectKeyOrId=someProjectKey'); 928 | }); 929 | 930 | it('createBoard hits proper url', async () => { 931 | const result = await dummyURLCall('createBoard'); 932 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board'); 933 | }); 934 | 935 | it('getBoard hits proper url', async () => { 936 | const result = await dummyURLCall('getBoard', ['someBoardId']); 937 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId'); 938 | }); 939 | 940 | it('deleteBoard hits proper url', async () => { 941 | const result = await dummyURLCall('deleteBoard', ['someBoardId']); 942 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId'); 943 | }); 944 | 945 | it('getIssuesForBacklog hits proper url', async () => { 946 | const result = await dummyURLCall('getIssuesForBacklog', ['someBoardId']); 947 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/backlog?startAt=0&maxResults=50&jql=&validateQuery=true&fields='); 948 | }); 949 | 950 | it('getConfiguration hits proper url', async () => { 951 | const result = await dummyURLCall('getConfiguration', ['someBoardId']); 952 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/configuration'); 953 | }); 954 | 955 | it('getIssuesForBoard hits proper url', async () => { 956 | const result = await dummyURLCall('getIssuesForBoard', ['someBoardId']); 957 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/issue?startAt=0&maxResults=50&jql=&validateQuery=true&fields='); 958 | }); 959 | 960 | it('getFilter hits proper url', async () => { 961 | const result = await dummyURLCall('getFilter', ['someFilterId']); 962 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/filter/someFilterId'); 963 | }); 964 | 965 | it('getEpics hits proper url', async () => { 966 | const result = await dummyURLCall('getEpics', ['someBoardId']); 967 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/epic?startAt=0&maxResults=50&done='); 968 | }); 969 | 970 | it('getBoardIssuesForEpic hits proper url', async () => { 971 | const result = await dummyURLCall('getBoardIssuesForEpic', ['someBoardId', 'someEpicId']); 972 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/epic/someEpicId/issue?startAt=0&maxResults=50&jql=&validateQuery=true&fields='); 973 | }); 974 | 975 | it('getProjects hits proper url', async () => { 976 | const result = await dummyURLCall('getProjects', ['someBoardId']); 977 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/project?startAt=0&maxResults=50'); 978 | }); 979 | 980 | it('getProjectsFull hits proper url', async () => { 981 | const result = await dummyURLCall('getProjectsFull', ['someBoardId']); 982 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/project/full'); 983 | }); 984 | 985 | it('getBoardPropertiesKeys hits proper url', async () => { 986 | const result = await dummyURLCall('getBoardPropertiesKeys', ['someBoardId']); 987 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/properties'); 988 | }); 989 | 990 | it('deleteBoardProperty hits proper url', async () => { 991 | const result = await dummyURLCall('deleteBoardProperty', ['someBoardId', 'somePropertyKey']); 992 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/properties/somePropertyKey'); 993 | }); 994 | 995 | it('setBoardProperty hits proper url', async () => { 996 | const result = await dummyURLCall('setBoardProperty', ['someBoardId', 'somePropertyKey']); 997 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/properties/somePropertyKey'); 998 | }); 999 | 1000 | it('getBoardProperty hits proper url', async () => { 1001 | const result = await dummyURLCall('getBoardProperty', ['someBoardId', 'somePropertyKey']); 1002 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/properties/somePropertyKey'); 1003 | }); 1004 | 1005 | it('getAllSprints hits proper url', async () => { 1006 | const result = await dummyURLCall('getAllSprints', ['someBoardId']); 1007 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/sprint?startAt=0&maxResults=50&state='); 1008 | }); 1009 | 1010 | it('getBoardIssuesForSprint hits proper url', async () => { 1011 | const result = await dummyURLCall('getBoardIssuesForSprint', ['someBoardId', 'someSprintId']); 1012 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/sprint/someSprintId/issue?startAt=0&maxResults=50&jql=&validateQuery=true&fields=&expand='); 1013 | }); 1014 | 1015 | it('getAllVersions hits proper url', async () => { 1016 | const result = await dummyURLCall('getAllVersions', ['someBoardId']); 1017 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/board/someBoardId/version?startAt=0&maxResults=50&released='); 1018 | }); 1019 | 1020 | it('getEpic hits proper url', async () => { 1021 | const result = await dummyURLCall('getEpic', ['someEpicId']); 1022 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/epic/someEpicId'); 1023 | }); 1024 | 1025 | it('partiallyUpdateEpic hits proper url', async () => { 1026 | const result = await dummyURLCall('partiallyUpdateEpic', ['someEpicId']); 1027 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/epic/someEpicId'); 1028 | }); 1029 | 1030 | it('getIssuesForEpic hits proper url', async () => { 1031 | const result = await dummyURLCall('getIssuesForEpic', ['someEpicId']); 1032 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/epic/someEpicId/issue?startAt=0&maxResults=50&jql=&validateQuery=true&fields='); 1033 | }); 1034 | 1035 | it('moveIssuesToEpic hits proper url', async () => { 1036 | const result = await dummyURLCall('moveIssuesToEpic', ['someEpicId']); 1037 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/epic/someEpicId/issue'); 1038 | }); 1039 | 1040 | it('rankEpics hits proper url', async () => { 1041 | const result = await dummyURLCall('rankEpics', ['someEpicId']); 1042 | result.should.eql('http://jira.somehost.com:8080/rest/agile/1.0/epic/someEpicId/rank'); 1043 | }); 1044 | }); 1045 | 1046 | it('issueNotify hits proper url', async () => { 1047 | const result = await dummyURLCall('issueNotify', ['someIssueId', {}]); 1048 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/someIssueId/notify'); 1049 | }); 1050 | 1051 | it('getServerInfo hits proper url', async () => { 1052 | const result = await dummyURLCall('getServerInfo'); 1053 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/serverInfo'); 1054 | }); 1055 | 1056 | it('moveVersion hits proper url', async () => { 1057 | const result = await dummyURLCall('moveVersion', ['myVersion']); 1058 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/version/myVersion/move'); 1059 | }); 1060 | 1061 | it('getIssueCreateMetadata hits proper url', async () => { 1062 | const result = await dummyURLCall('getIssueCreateMetadata', [{ expand: 'projects.issuetypes.fields' }]); 1063 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/issue/createmeta?expand=projects.issuetypes.fields'); 1064 | }); 1065 | 1066 | it('genericGet hits proper url', async () => { 1067 | // Test with field endpoint as a simple example 1068 | const result = await dummyURLCall('genericGet', ['field']); 1069 | result.should.eql('http://jira.somehost.com:8080/rest/api/2.0/field'); 1070 | }); 1071 | }); 1072 | }); 1073 | -------------------------------------------------------------------------------- /src/jira.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | // Need to use require here for testing 4 | // eslint-disable-next-line no-underscore-dangle 5 | const _request = require('postman-request'); 6 | 7 | function request(uri, options) { 8 | return new Promise((resolve, reject) => { 9 | _request(uri, options, (err, httpResponse) => { 10 | if (err) { 11 | reject(err); 12 | } else { 13 | if (httpResponse.statusCode >= 400) { 14 | reject(httpResponse.body); 15 | } 16 | 17 | // for compatibility with request-promise 18 | resolve(httpResponse.body); 19 | } 20 | }); 21 | }); 22 | } 23 | /** 24 | * @name JiraApi 25 | * @class 26 | * Wrapper for the JIRA Rest Api 27 | * https://docs.atlassian.com/jira/REST/6.4.8/ 28 | */ 29 | export default class JiraApi { 30 | /** 31 | * @constructor 32 | * @function 33 | * @param {JiraApiOptions} options 34 | */ 35 | constructor(options) { 36 | this.protocol = options.protocol || 'http'; 37 | this.host = options.host; 38 | this.port = options.port || null; 39 | this.apiVersion = options.apiVersion || '2'; 40 | this.base = options.base || ''; 41 | this.intermediatePath = options.intermediatePath; 42 | this.strictSSL = options.hasOwnProperty('strictSSL') ? options.strictSSL : true; 43 | // This is so we can fake during unit tests 44 | this.request = options.request || request; 45 | this.webhookVersion = options.webHookVersion || '1.0'; 46 | this.greenhopperVersion = options.greenhopperVersion || '1.0'; 47 | this.baseOptions = {}; 48 | 49 | if (options.ca) { 50 | this.baseOptions.ca = options.ca; 51 | } 52 | 53 | if (options.oauth && options.oauth.consumer_key && options.oauth.access_token) { 54 | this.baseOptions.oauth = { 55 | consumer_key: options.oauth.consumer_key, 56 | consumer_secret: options.oauth.consumer_secret, 57 | token: options.oauth.access_token, 58 | token_secret: options.oauth.access_token_secret, 59 | signature_method: options.oauth.signature_method || 'RSA-SHA1', 60 | }; 61 | } else if (options.bearer) { 62 | this.baseOptions.auth = { 63 | user: '', 64 | pass: '', 65 | sendImmediately: true, 66 | bearer: options.bearer, 67 | }; 68 | } else if (options.username && options.password) { 69 | this.baseOptions.auth = { 70 | user: options.username, 71 | pass: options.password, 72 | }; 73 | } 74 | 75 | if (options.timeout) { 76 | this.baseOptions.timeout = options.timeout; 77 | } 78 | } 79 | 80 | /** 81 | * @typedef JiraApiOptions 82 | * @type {object} 83 | * @property {string} [protocol=http] - What protocol to use to connect to 84 | * jira? Ex: http|https 85 | * @property {string} host - What host is this tool connecting to for the jira 86 | * instance? Ex: jira.somehost.com 87 | * @property {string} [port] - What port is this tool connecting to jira with? Only needed for 88 | * none standard ports. Ex: 8080, 3000, etc 89 | * @property {string} [username] - Specify a username for this tool to authenticate all 90 | * requests with. 91 | * @property {string} [password] - Specify a password for this tool to authenticate all 92 | * requests with. Cloud users need to generate an [API token](https://confluence.atlassian.com/cloud/api-tokens-938839638.html) for this value. 93 | * @property {string} [apiVersion=2] - What version of the jira rest api is the instance the 94 | * tool is connecting to? 95 | * @property {string} [base] - What other url parts exist, if any, before the rest/api/ 96 | * section? 97 | * @property {string} [intermediatePath] - If specified, overwrites the default rest/api/version 98 | * section of the uri 99 | * @property {boolean} [strictSSL=true] - Does this tool require each request to be 100 | * authenticated? Defaults to true. 101 | * @property {function} [request] - What method does this tool use to make its requests? 102 | * Defaults to request from request-promise 103 | * @property {number} [timeout] - Integer containing the number of milliseconds to wait for a 104 | * server to send response headers (and start the response body) before aborting the request. Note 105 | * that if the underlying TCP connection cannot be established, the OS-wide TCP connection timeout 106 | * will overrule the timeout option ([the default in Linux can be anywhere from 20-120 * 107 | * seconds](http://www.sekuda.com/overriding_the_default_linux_kernel_20_second_tcp_socket_connect_timeout)). 108 | * @property {string} [webhookVersion=1.0] - What webhook version does this api wrapper need to 109 | * hit? 110 | * @property {string} [greenhopperVersion=1.0] - What webhook version does this api wrapper need 111 | * to hit? 112 | * @property {string} [ca] - Specify a CA certificate 113 | * @property {OAuth} [oauth] - Specify an OAuth object for this tool to authenticate all requests 114 | * using OAuth. 115 | * @property {string} [bearer] - Specify an OAuth bearer token to authenticate all requests with. 116 | */ 117 | 118 | /** 119 | * @typedef OAuth 120 | * @type {object} 121 | * @property {string} consumer_key - The consumer entered in Jira Preferences. 122 | * @property {string} consumer_secret - The private RSA file. 123 | * @property {string} access_token - The generated access token. 124 | * @property {string} access_token_secret - The generated access toke secret. 125 | * @property {string} signature_method [signature_method=RSA-SHA1] - OAuth signurate methode 126 | * Possible values RSA-SHA1, HMAC-SHA1, PLAINTEXT. Jira Cloud supports only RSA-SHA1. 127 | */ 128 | 129 | /** 130 | * @typedef {object} UriOptions 131 | * @property {string} pathname - The url after the specific functions path 132 | * @property {object} [query] - An object of all query parameters 133 | * @property {string} [intermediatePath] - Overwrites with specified path 134 | */ 135 | 136 | /** 137 | * @name makeRequestHeader 138 | * @function 139 | * Creates a requestOptions object based on the default template for one 140 | * @param {string} uri 141 | * @param {object} [options] - an object containing fields and formatting how the 142 | */ 143 | makeRequestHeader(uri, options = {}) { 144 | return { 145 | rejectUnauthorized: this.strictSSL, 146 | method: options.method || 'GET', 147 | uri, 148 | json: true, 149 | ...options, 150 | }; 151 | } 152 | 153 | /** 154 | * @typedef makeRequestHeaderOptions 155 | * @type {object} 156 | * @property {string} [method] - HTTP Request Method. ie GET, POST, PUT, DELETE 157 | */ 158 | 159 | /** 160 | * @name makeUri 161 | * @function 162 | * Creates a URI object for a given pathname 163 | * @param {object} [options] - an object containing path information 164 | */ 165 | makeUri({ 166 | pathname, query, intermediatePath, encode = false, 167 | }) { 168 | const intermediateToUse = this.intermediatePath || intermediatePath; 169 | const tempPath = intermediateToUse || `/rest/api/${this.apiVersion}`; 170 | const uri = url.format({ 171 | protocol: this.protocol, 172 | hostname: this.host, 173 | port: this.port, 174 | pathname: `${this.base}${tempPath}${pathname}`, 175 | query, 176 | }); 177 | return encode ? encodeURI(uri) : decodeURIComponent(uri); 178 | } 179 | 180 | /** 181 | * @typedef makeUriOptions 182 | * @type {object} 183 | * @property {string} pathname - The url after the /rest/api/version 184 | * @property {object} query - a query object 185 | * @property {string} intermediatePath - If specified will overwrite the /rest/api/version section 186 | */ 187 | 188 | /** 189 | * @name makeWebhookUri 190 | * @function 191 | * Creates a URI object for a given pathName 192 | * @param {object} [options] - An options object specifying uri information 193 | */ 194 | makeWebhookUri({ pathname, intermediatePath }) { 195 | const intermediateToUse = this.intermediatePath || intermediatePath; 196 | const tempPath = intermediateToUse || `/rest/webhooks/${this.webhookVersion}`; 197 | const uri = url.format({ 198 | protocol: this.protocol, 199 | hostname: this.host, 200 | port: this.port, 201 | pathname: `${this.base}${tempPath}${pathname}`, 202 | }); 203 | return decodeURIComponent(uri); 204 | } 205 | 206 | /** 207 | * @typedef makeWebhookUriOptions 208 | * @type {object} 209 | * @property {string} pathname - The url after the /rest/webhooks 210 | * @property {string} intermediatePath - If specified will overwrite the /rest/webhooks section 211 | */ 212 | 213 | /** 214 | * @name makeSprintQueryUri 215 | * @function 216 | * Creates a URI object for a given pathName 217 | * @param {object} [options] - The url after the /rest/ 218 | */ 219 | makeSprintQueryUri({ pathname, query, intermediatePath }) { 220 | const intermediateToUse = this.intermediatePath || intermediatePath; 221 | const tempPath = intermediateToUse || `/rest/greenhopper/${this.greenhopperVersion}`; 222 | const uri = url.format({ 223 | protocol: this.protocol, 224 | hostname: this.host, 225 | port: this.port, 226 | pathname: `${this.base}${tempPath}${pathname}`, 227 | query, 228 | }); 229 | return decodeURIComponent(uri); 230 | } 231 | 232 | /** 233 | * @typedef makeSprintQueryUriOptions 234 | * @type {object} 235 | * @property {string} pathname - The url after the /rest/api/version 236 | * @property {object} query - a query object 237 | * @property {string} intermediatePath - will overwrite the /rest/greenhopper/version section 238 | */ 239 | 240 | /** 241 | * @typedef makeDevStatusUri 242 | * @function 243 | * Creates a URI object for a given pathname 244 | * @arg {pathname, query, intermediatePath} obj1 245 | * @param {string} pathname obj1.pathname - The url after the /rest/api/version 246 | * @param {object} query obj1.query - a query object 247 | * @param {string} intermediatePath obj1.intermediatePath - If specified will overwrite the 248 | * /rest/dev-status/latest/issue/detail section 249 | */ 250 | makeDevStatusUri({ pathname, query, intermediatePath }) { 251 | const intermediateToUse = this.intermediatePath || intermediatePath; 252 | const tempPath = intermediateToUse || '/rest/dev-status/latest/issue'; 253 | const uri = url.format({ 254 | protocol: this.protocol, 255 | hostname: this.host, 256 | port: this.port, 257 | pathname: `${this.base}${tempPath}${pathname}`, 258 | query, 259 | }); 260 | return decodeURIComponent(uri); 261 | } 262 | 263 | /** 264 | * @name makeAgile1Uri 265 | * @function 266 | * Creates a URI object for a given pathname 267 | * @param {UriOptions} object 268 | */ 269 | makeAgileUri(object) { 270 | const intermediateToUse = this.intermediatePath || object.intermediatePath; 271 | const tempPath = intermediateToUse || '/rest/agile/1.0'; 272 | const uri = url.format({ 273 | protocol: this.protocol, 274 | hostname: this.host, 275 | port: this.port, 276 | pathname: `${this.base}${tempPath}${object.pathname}`, 277 | query: object.query, 278 | }); 279 | return decodeURIComponent(uri); 280 | } 281 | 282 | /** 283 | * @name doRequest 284 | * @function 285 | * Does a request based on the requestOptions object 286 | * @param {object} requestOptions - fields on this object get posted as a request header for 287 | * requests to jira 288 | */ 289 | async doRequest(requestOptions) { 290 | const options = { 291 | ...this.baseOptions, 292 | ...requestOptions, 293 | }; 294 | 295 | try { 296 | const response = await this.request(options); 297 | 298 | if (response) { 299 | if (Array.isArray(response.errorMessages) && response.errorMessages.length > 0) { 300 | throw new Error(response.errorMessages.join(', ')); 301 | } 302 | } 303 | 304 | return response; 305 | } catch (e) { 306 | throw new Error(JSON.stringify(e)); 307 | } 308 | } 309 | 310 | /** 311 | * @name findIssue 312 | * @function 313 | * Find an issue in jira 314 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290709) 315 | * @param {string} issueNumber - The issue number to search for including the project key 316 | * @param {string} expand - The resource expansion to return additional fields in the response 317 | * @param {string} fields - Comma separated list of field ids or keys to retrieve 318 | * @param {string} properties - Comma separated list of properties to retrieve 319 | * @param {boolean} fieldsByKeys - False by default, used to retrieve fields by key instead of id 320 | */ 321 | findIssue(issueNumber, expand, fields, properties, fieldsByKeys) { 322 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 323 | pathname: `/issue/${issueNumber}`, 324 | query: { 325 | expand: expand || '', 326 | fields: fields || '*all', 327 | properties: properties || '*all', 328 | fieldsByKeys: fieldsByKeys || false, 329 | }, 330 | }))); 331 | } 332 | 333 | /** 334 | * @name downloadAttachment 335 | * @function 336 | * Download an attachment 337 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id288524) 338 | * @param {object} attachment - the attachment 339 | */ 340 | downloadAttachment(attachment) { 341 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 342 | pathname: `/attachment/${attachment.id}/${attachment.filename}`, 343 | intermediatePath: '/secure', 344 | encode: true, 345 | }), { json: false, encoding: null })); 346 | } 347 | 348 | /** 349 | * @name deleteAttachment 350 | * @function 351 | * Remove the attachment 352 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-attachment-id-delete) 353 | * @param {string} attachmentId - the attachment id 354 | */ 355 | deleteAttachment(attachmentId) { 356 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 357 | pathname: `/attachment/${attachmentId}`, 358 | }), { method: 'DELETE', json: false, encoding: null })); 359 | } 360 | 361 | /** 362 | * @name getUnresolvedIssueCount 363 | * @function 364 | * Get the unresolved issue count 365 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id288524) 366 | * @param {string} version - the version of your product you want to find the unresolved 367 | * issues of. 368 | */ 369 | async getUnresolvedIssueCount(version) { 370 | const requestHeaders = this.makeRequestHeader( 371 | this.makeUri({ 372 | pathname: `/version/${version}/unresolvedIssueCount`, 373 | }), 374 | ); 375 | const response = await this.doRequest(requestHeaders); 376 | return response.issuesUnresolvedCount; 377 | } 378 | 379 | /** 380 | * @name getProject 381 | * @function 382 | * Get the Project by project key 383 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id289232) 384 | * @param {string} project - key for the project 385 | */ 386 | getProject(project) { 387 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 388 | pathname: `/project/${project}`, 389 | }))); 390 | } 391 | 392 | /** 393 | * @name createProject 394 | * @function 395 | * Create a new Project 396 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#api/2/project-createProject) 397 | * @param {object} project - with specs 398 | */ 399 | createProject(project) { 400 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 401 | pathname: '/project/', 402 | }), { 403 | method: 'POST', 404 | body: project, 405 | })); 406 | } 407 | 408 | /** Find the Rapid View for a specified project 409 | * @name findRapidView 410 | * @function 411 | * @param {string} projectName - name for the project 412 | */ 413 | async findRapidView(projectName) { 414 | const response = await this.doRequest(this.makeRequestHeader(this.makeSprintQueryUri({ 415 | pathname: '/rapidviews/list', 416 | }))); 417 | 418 | if (typeof projectName === 'undefined' || projectName === null) return response.views; 419 | 420 | const rapidViewResult = response.views 421 | .find((x) => x.name.toLowerCase() === projectName.toLowerCase()); 422 | 423 | return rapidViewResult; 424 | } 425 | 426 | /** Get the most recent sprint for a given rapidViewId 427 | * @name getLastSprintForRapidView 428 | * @function 429 | * @param {string} rapidViewId - the id for the rapid view 430 | */ 431 | async getLastSprintForRapidView(rapidViewId) { 432 | const response = await this.doRequest( 433 | this.makeRequestHeader(this.makeSprintQueryUri({ 434 | pathname: `/sprintquery/${rapidViewId}`, 435 | })), 436 | ); 437 | return response.sprints.pop(); 438 | } 439 | 440 | /** Get the issues for a rapidView / sprint 441 | * @name getSprintIssues 442 | * @function 443 | * @param {string} rapidViewId - the id for the rapid view 444 | * @param {string} sprintId - the id for the sprint 445 | */ 446 | getSprintIssues(rapidViewId, sprintId) { 447 | return this.doRequest(this.makeRequestHeader(this.makeSprintQueryUri({ 448 | pathname: '/rapid/charts/sprintreport', 449 | query: { 450 | rapidViewId, 451 | sprintId, 452 | }, 453 | }))); 454 | } 455 | 456 | /** Get a list of Sprints belonging to a Rapid View 457 | * @name listSprints 458 | * @function 459 | * @param {string} rapidViewId - the id for the rapid view 460 | */ 461 | listSprints(rapidViewId) { 462 | return this.doRequest(this.makeRequestHeader(this.makeSprintQueryUri({ 463 | pathname: `/sprintquery/${rapidViewId}`, 464 | }))); 465 | } 466 | 467 | /** Get details about a Sprint 468 | * @name getSprint 469 | * @function 470 | * @param {string} sprintId - the id for the sprint view 471 | */ 472 | getSprint(sprintId) { 473 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 474 | pathname: `/sprint/${sprintId}`, 475 | }))); 476 | } 477 | 478 | /** Add an issue to the project's current sprint 479 | * @name addIssueToSprint 480 | * @function 481 | * @param {string} issueId - the id of the existing issue 482 | * @param {string} sprintId - the id of the sprint to add it to 483 | */ 484 | addIssueToSprint(issueId, sprintId) { 485 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 486 | pathname: `/sprint/${sprintId}/issue`, 487 | }), { 488 | method: 'POST', 489 | body: { 490 | issues: [issueId], 491 | }, 492 | })); 493 | } 494 | 495 | /** Create an issue link between two issues 496 | * @name issueLink 497 | * @function 498 | * @param {object} link - a link object formatted how the Jira API specifies 499 | */ 500 | issueLink(link) { 501 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 502 | pathname: '/issueLink', 503 | }), { 504 | method: 'POST', 505 | followAllRedirects: true, 506 | body: link, 507 | })); 508 | } 509 | 510 | /** List all issue link types jira knows about 511 | * [Jira Doc](https://docs.atlassian.com/software/jira/docs/api/REST/8.5.0/#api/2/issueLinkType-getIssueLinkTypes) 512 | * @name listIssueLinkTypes 513 | * @function 514 | */ 515 | listIssueLinkTypes() { 516 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 517 | pathname: '/issueLinkType', 518 | }))); 519 | } 520 | 521 | /** Retrieves the remote links associated with the given issue. 522 | * @name getRemoteLinks 523 | * @function 524 | * @param {string} issueNumber - the issue number to find remote links for. 525 | */ 526 | getRemoteLinks(issueNumber) { 527 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 528 | pathname: `/issue/${issueNumber}/remotelink`, 529 | }))); 530 | } 531 | 532 | /** 533 | * @name createRemoteLink 534 | * @function 535 | * Creates a remote link associated with the given issue. 536 | * @param {string} issueNumber - The issue number to create the remotelink under 537 | * @param {object} remoteLink - the remotelink object as specified by the Jira API 538 | */ 539 | createRemoteLink(issueNumber, remoteLink) { 540 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 541 | pathname: `/issue/${issueNumber}/remotelink`, 542 | }), { 543 | method: 'POST', 544 | body: remoteLink, 545 | })); 546 | } 547 | 548 | /** 549 | * @name deleteRemoteLink 550 | * @function 551 | * Delete a remote link with given issueNumber and id 552 | * @param {string} issueNumber - The issue number to delete the remotelink under 553 | * @param {string} id the remotelink id 554 | */ 555 | deleteRemoteLink(issueNumber, id) { 556 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 557 | pathname: `/issue/${issueNumber}/remotelink/${id}`, 558 | }), { 559 | method: 'DELETE', 560 | followAllRedirects: true, 561 | })); 562 | } 563 | 564 | /** Get Versions for a project 565 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id289653) 566 | * @name getVersions 567 | * @function 568 | * @param {string} project - A project key to get versions for 569 | * @param {object} query - An object containing the query params 570 | */ 571 | getVersions(project, query = {}) { 572 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 573 | pathname: `/project/${project}/versions`, 574 | query, 575 | }))); 576 | } 577 | 578 | /** Get details of single Version in project 579 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/version-getVersion) 580 | * @name getVersion 581 | * @function 582 | * @param {string} version - The id of this version 583 | */ 584 | getVersion(version) { 585 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 586 | pathname: `/version/${version}`, 587 | }))); 588 | } 589 | 590 | /** Create a version 591 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id288232) 592 | * @name createVersion 593 | * @function 594 | * @param {object} version - an object of the new version 595 | */ 596 | createVersion(version) { 597 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 598 | pathname: '/version', 599 | }), { 600 | method: 'POST', 601 | followAllRedirects: true, 602 | body: version, 603 | })); 604 | } 605 | 606 | /** Update a version 607 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#d2e510) 608 | * @name updateVersion 609 | * @function 610 | * @param {object} version - an new object of the version to update 611 | */ 612 | updateVersion(version) { 613 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 614 | pathname: `/version/${version.id}`, 615 | }), { 616 | method: 'PUT', 617 | followAllRedirects: true, 618 | body: version, 619 | })); 620 | } 621 | 622 | /** Delete a version 623 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#api/2/version-delete) 624 | * @name deleteVersion 625 | * @function 626 | * @param {string} versionId - the ID of the version to delete 627 | * @param {string} moveFixIssuesToId - when provided, existing fixVersions will be moved 628 | * to this ID. Otherwise, the deleted version will be removed from all 629 | * issue fixVersions. 630 | * @param {string} moveAffectedIssuesToId - when provided, existing affectedVersions will 631 | * be moved to this ID. Otherwise, the deleted version will be removed 632 | * from all issue affectedVersions. 633 | */ 634 | deleteVersion(versionId, moveFixIssuesToId, moveAffectedIssuesToId) { 635 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 636 | pathname: `/version/${versionId}`, 637 | }), { 638 | method: 'DELETE', 639 | followAllRedirects: true, 640 | qs: { 641 | moveFixIssuesTo: moveFixIssuesToId, 642 | moveAffectedIssuesTo: moveAffectedIssuesToId, 643 | }, 644 | })); 645 | } 646 | 647 | /** Move version 648 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/version-moveVersion) 649 | * @name moveVersion 650 | * @function 651 | * @param {string} versionId - the ID of the version to delete 652 | * @param {string} position - an object of the new position 653 | */ 654 | 655 | moveVersion(versionId, position) { 656 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 657 | pathname: `/version/${versionId}/move`, 658 | }), { 659 | method: 'POST', 660 | followAllRedirects: true, 661 | body: position, 662 | })); 663 | } 664 | 665 | /** Pass a search query to Jira 666 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#d2e4424) 667 | * @name searchJira 668 | * @function 669 | * @param {string} searchString - jira query string in JQL 670 | * @param {object} optional - object containing any of the following properties 671 | * @param {integer} [optional.startAt=0]: optional starting index number 672 | * @param {integer} [optional.maxResults=50]: optional The maximum number of items to 673 | * return per page. To manage page size, Jira may return fewer items per 674 | * page where a large number of fields are requested. 675 | * @param {array} [optional.fields]: optional array of string names of desired fields 676 | * @param {array} [optional.expand]: optional array of string names of desired expand nodes 677 | */ 678 | searchJira(searchString, optional = {}) { 679 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 680 | pathname: '/search', 681 | }), { 682 | method: 'POST', 683 | followAllRedirects: true, 684 | body: { 685 | jql: searchString, 686 | ...optional, 687 | }, 688 | })); 689 | } 690 | 691 | /** Create a Jira user 692 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser) 693 | * @name createUser 694 | * @function 695 | * @param {object} user - Properly Formatted User object 696 | */ 697 | createUser(user) { 698 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 699 | pathname: '/user', 700 | }), { 701 | method: 'POST', 702 | followAllRedirects: true, 703 | body: user, 704 | })); 705 | } 706 | 707 | /** Search user on Jira 708 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#d2e3756) 709 | * @name searchUsers 710 | * @function 711 | * @param {SearchUserOptions} options 712 | */ 713 | searchUsers({ 714 | username, query, startAt, maxResults, includeActive, includeInactive, 715 | }) { 716 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 717 | pathname: '/user/search', 718 | query: { 719 | username, 720 | query, 721 | startAt: startAt || 0, 722 | maxResults: maxResults || 50, 723 | includeActive: includeActive || true, 724 | includeInactive: includeInactive || false, 725 | }, 726 | }), { 727 | followAllRedirects: true, 728 | })); 729 | } 730 | 731 | /** 732 | * @typedef SearchUserOptions 733 | * @type {object} 734 | * @property {string} username - (DEPRECATED) A query string used to search username, name or 735 | * e-mail address 736 | * @property {string} query - A query string that is matched against user attributes 737 | * (displayName, and emailAddress) to find relevant users. The string can match the prefix of 738 | * the attribute's value. For example, query=john matches a user with a displayName of John 739 | * Smith and a user with an emailAddress of johnson@example.com. Required, unless accountId 740 | * or property is specified. 741 | * @property {integer} [startAt=0] - The index of the first user to return (0-based) 742 | * @property {integer} [maxResults=50] - The maximum number of users to return 743 | * @property {boolean} [includeActive=true] - If true, then active users are included 744 | * in the results 745 | * @property {boolean} [includeInactive=false] - If true, then inactive users 746 | * are included in the results 747 | */ 748 | 749 | /** Get all users in group on Jira 750 | * @name getUsersInGroup 751 | * @function 752 | * @param {string} groupname - A query string used to search users in group 753 | * @param {integer} [startAt=0] - The index of the first user to return (0-based) 754 | * @param {integer} [maxResults=50] - The maximum number of users to return (defaults to 50). 755 | * @deprecated 756 | */ 757 | getUsersInGroup(groupname, startAt = 0, maxResults = 50) { 758 | return this.doRequest( 759 | this.makeRequestHeader(this.makeUri({ 760 | pathname: '/group', 761 | query: { 762 | groupname, 763 | expand: `users[${startAt}:${maxResults}]`, 764 | }, 765 | }), { 766 | followAllRedirects: true, 767 | }), 768 | ); 769 | } 770 | 771 | /** Get all members of group on Jira 772 | * @name getMembersOfGroup 773 | * @function 774 | * @param {string} groupname - A query string used to search users in group 775 | * @param {integer} [startAt=0] - The index of the first user to return (0-based) 776 | * @param {integer} [maxResults=50] - The maximum number of users to return (defaults to 50). 777 | * @param {boolean} [includeInactiveUsers=false] - Fetch inactive users too (defaults to false). 778 | */ 779 | getMembersOfGroup(groupname, startAt = 0, maxResults = 50, includeInactiveUsers = false) { 780 | return this.doRequest( 781 | this.makeRequestHeader(this.makeUri({ 782 | pathname: '/group/member', 783 | query: { 784 | groupname, 785 | expand: `users[${startAt}:${maxResults}]`, 786 | includeInactiveUsers, 787 | }, 788 | }), { 789 | followAllRedirects: true, 790 | }), 791 | ); 792 | } 793 | 794 | /** Get issues related to a user 795 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id296043) 796 | * @name getUsersIssues 797 | * @function 798 | * @param {string} username - username of user to search for 799 | * @param {boolean} open - determines if only open issues should be returned 800 | */ 801 | getUsersIssues(username, open) { 802 | const openJql = open ? ' AND status in (Open, \'In Progress\', Reopened)' : ''; 803 | return this.searchJira(`assignee = ${username.replace('@', '\\u0040')}${openJql}`, {}); 804 | } 805 | 806 | /** Returns a user. 807 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-user-get) 808 | * @name getUser 809 | * @function 810 | * @param {string} accountId - The accountId of user to search for 811 | * @param {string} expand - The expand for additional info (groups,applicationRoles) 812 | */ 813 | getUser(accountId, expand) { 814 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 815 | pathname: '/user', 816 | query: { 817 | accountId, 818 | expand, 819 | }, 820 | }))); 821 | } 822 | 823 | /** Returns a list of all (active and inactive) users. 824 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-users-search-get) 825 | * @name getUsers 826 | * @function 827 | * @param {integer} [startAt=0] - The index of the first user to return (0-based) 828 | * @param {integer} [maxResults=50] - The maximum number of users to return (defaults to 50). 829 | */ 830 | getUsers(startAt = 0, maxResults = 100) { 831 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 832 | pathname: '/users', 833 | query: { 834 | startAt, 835 | maxResults, 836 | }, 837 | }))); 838 | } 839 | 840 | /** Add issue to Jira 841 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290028) 842 | * @name addNewIssue 843 | * @function 844 | * @param {object} issue - Properly Formatted Issue object 845 | */ 846 | addNewIssue(issue) { 847 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 848 | pathname: '/issue', 849 | }), { 850 | method: 'POST', 851 | followAllRedirects: true, 852 | body: issue, 853 | })); 854 | } 855 | 856 | /** Add a user as a watcher on an issue 857 | * @name addWatcher 858 | * @function 859 | * @param {string} issueKey - the key of the existing issue 860 | * @param {string} username - the jira username to add as a watcher to the issue 861 | */ 862 | addWatcher(issueKey, username) { 863 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 864 | pathname: `/issue/${issueKey}/watchers`, 865 | }), { 866 | method: 'POST', 867 | followAllRedirects: true, 868 | body: username, 869 | })); 870 | } 871 | 872 | /** Change an assignee on an issue 873 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-assign) 874 | * @name assignee 875 | * @function 876 | * @param {string} issueKey - the key of the existing issue 877 | * @param {string} assigneeName - the jira username to add as a new assignee to the issue 878 | */ 879 | updateAssignee(issueKey, assigneeName) { 880 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 881 | pathname: `/issue/${issueKey}/assignee`, 882 | }), { 883 | method: 'PUT', 884 | followAllRedirects: true, 885 | body: { name: assigneeName }, 886 | })); 887 | } 888 | 889 | /** Change an assignee on an issue 890 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-assignee-put) 891 | * @name updateAssigneeWithId 892 | * @function 893 | * @param {string} issueKey - the key of the existing issue 894 | * @param {string} userId - the jira username to add as a new assignee to the issue 895 | */ 896 | updateAssigneeWithId(issueKey, userId) { 897 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 898 | pathname: `/issue/${issueKey}/assignee`, 899 | }), { 900 | method: 'PUT', 901 | followAllRedirects: true, 902 | body: { accountId: userId }, 903 | })); 904 | } 905 | 906 | /** Delete issue from Jira 907 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290791) 908 | * @name deleteIssue 909 | * @function 910 | * @param {string} issueId - the Id of the issue to delete 911 | */ 912 | deleteIssue(issueId) { 913 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 914 | pathname: `/issue/${issueId}`, 915 | }), { 916 | method: 'DELETE', 917 | followAllRedirects: true, 918 | })); 919 | } 920 | 921 | /** Update issue in Jira 922 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290878) 923 | * @name updateIssue 924 | * @function 925 | * @param {string} issueId - the Id of the issue to update 926 | * @param {object} issueUpdate - update Object as specified by the rest api 927 | * @param {object} query - adds parameters to the query string 928 | */ 929 | updateIssue(issueId, issueUpdate, query = {}) { 930 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 931 | pathname: `/issue/${issueId}`, 932 | query, 933 | }), { 934 | body: issueUpdate, 935 | method: 'PUT', 936 | followAllRedirects: true, 937 | })); 938 | } 939 | 940 | /** List Components 941 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 942 | * @name listComponents 943 | * @function 944 | * @param {string} project - key for the project 945 | */ 946 | listComponents(project) { 947 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 948 | pathname: `/project/${project}/components`, 949 | }))); 950 | } 951 | 952 | /** Add component to Jira 953 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290028) 954 | * @name addNewComponent 955 | * @function 956 | * @param {object} component - Properly Formatted Component 957 | */ 958 | addNewComponent(component) { 959 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 960 | pathname: '/component', 961 | }), { 962 | method: 'POST', 963 | followAllRedirects: true, 964 | body: component, 965 | })); 966 | } 967 | 968 | /** Update Jira component 969 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#api/2/component-updateComponent) 970 | * @name updateComponent 971 | * @function 972 | * @param {string} componentId - the Id of the component to update 973 | * @param {object} component - Properly Formatted Component 974 | */ 975 | updateComponent(componentId, component) { 976 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 977 | pathname: `/component/${componentId}`, 978 | }), { 979 | method: 'PUT', 980 | followAllRedirects: true, 981 | body: component, 982 | })); 983 | } 984 | 985 | /** Delete component from Jira 986 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-api-2-component-id-delete) 987 | * @name deleteComponent 988 | * @function 989 | * @param {string} id - The ID of the component. 990 | * @param {string} moveIssuesTo - The ID of the component to replace the deleted component. 991 | * If this value is null no replacement is made. 992 | */ 993 | deleteComponent(id, moveIssuesTo) { 994 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 995 | pathname: `/component/${id}`, 996 | }), { 997 | method: 'DELETE', 998 | followAllRedirects: true, 999 | qs: moveIssuesTo ? { moveIssuesTo } : null, 1000 | })); 1001 | } 1002 | 1003 | /** Get count of issues assigned to the component. 1004 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-component-id-relatedIssueCounts-get) 1005 | * @name relatedIssueCounts 1006 | * @function 1007 | * @param {string} id - Component Id. 1008 | */ 1009 | relatedIssueCounts(id) { 1010 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1011 | pathname: `/component/${id}/relatedIssueCounts`, 1012 | }))); 1013 | } 1014 | 1015 | /** Create custom Jira field 1016 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#api/2/field-createCustomField) 1017 | * @name createCustomField 1018 | * @function 1019 | * @param {object} field - Properly formatted Field object 1020 | */ 1021 | createCustomField(field) { 1022 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1023 | pathname: '/field', 1024 | }), { 1025 | method: 'POST', 1026 | followAllRedirects: true, 1027 | body: field, 1028 | })); 1029 | } 1030 | 1031 | /** List all fields custom and not that jira knows about. 1032 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1033 | * @name listFields 1034 | * @function 1035 | */ 1036 | listFields() { 1037 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1038 | pathname: '/field', 1039 | }))); 1040 | } 1041 | 1042 | /** Add an option for a select list issue field. 1043 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#api/2/field/{fieldKey}/option-createOption) 1044 | * @name createFieldOption 1045 | * @function 1046 | * @param {string} fieldKey - the key of the select list field 1047 | * @param {object} option - properly formatted Option object 1048 | */ 1049 | createFieldOption(fieldKey, option) { 1050 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1051 | pathname: `/field/${fieldKey}/option`, 1052 | }), { 1053 | method: 'POST', 1054 | followAllRedirects: true, 1055 | body: option, 1056 | })); 1057 | } 1058 | 1059 | /** Returns all options defined for a select list issue field. 1060 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#api/2/field/{fieldKey}/option-getAllOptions) 1061 | * @name listFieldOptions 1062 | * @function 1063 | * @param {string} fieldKey - the key of the select list field 1064 | */ 1065 | listFieldOptions(fieldKey) { 1066 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1067 | pathname: `/field/${fieldKey}/option`, 1068 | }))); 1069 | } 1070 | 1071 | /** Creates or updates an option for a select list issue field. 1072 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#api/2/field/{fieldKey}/option-putOption) 1073 | * @name upsertFieldOption 1074 | * @function 1075 | * @param {string} fieldKey - the key of the select list field 1076 | * @param {string} optionId - the id of the modified option 1077 | * @param {object} option - properly formatted Option object 1078 | */ 1079 | upsertFieldOption(fieldKey, optionId, option) { 1080 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1081 | pathname: `/field/${fieldKey}/option/${optionId}`, 1082 | }), { 1083 | method: 'PUT', 1084 | followAllRedirects: true, 1085 | body: option, 1086 | })); 1087 | } 1088 | 1089 | /** Returns an option for a select list issue field. 1090 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#api/2/field/{fieldKey}/option-getOption) 1091 | * @name getFieldOption 1092 | * @function 1093 | * @param {string} fieldKey - the key of the select list field 1094 | * @param {string} optionId - the id of the option 1095 | */ 1096 | getFieldOption(fieldKey, optionId) { 1097 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1098 | pathname: `/field/${fieldKey}/option/${optionId}`, 1099 | }))); 1100 | } 1101 | 1102 | /** Deletes an option from a select list issue field. 1103 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#api/2/field/{fieldKey}/option-delete) 1104 | * @name deleteFieldOption 1105 | * @function 1106 | * @param {string} fieldKey - the key of the select list field 1107 | * @param {string} optionId - the id of the deleted option 1108 | */ 1109 | deleteFieldOption(fieldKey, optionId) { 1110 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1111 | pathname: `/field/${fieldKey}/option/${optionId}`, 1112 | }), { 1113 | method: 'DELETE', 1114 | followAllRedirects: true, 1115 | })); 1116 | } 1117 | 1118 | /** 1119 | * @name getIssueProperty 1120 | * @function 1121 | * Get Property of Issue by Issue and Property Id 1122 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/properties-getProperty) 1123 | * @param {string} issueNumber - The issue number to search for including the project key 1124 | * @param {string} property - The property key to search for 1125 | */ 1126 | getIssueProperty(issueNumber, property) { 1127 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1128 | pathname: `/issue/${issueNumber}/properties/${property}`, 1129 | }))); 1130 | } 1131 | 1132 | /** 1133 | * @name getIssueChangelog 1134 | * @function 1135 | * List all changes for an issue, sorted by date, starting from the latest 1136 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/changelog) 1137 | * @param {string} issueNumber - The issue number to search for including the project key 1138 | * @param {integer} [startAt=0] - optional starting index number 1139 | * @param {integer} [maxResults=50] - optional ending index number 1140 | */ 1141 | getIssueChangelog(issueNumber, startAt = 0, maxResults = 50) { 1142 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1143 | pathname: `/issue/${issueNumber}/changelog`, 1144 | query: { 1145 | startAt, 1146 | maxResults, 1147 | }, 1148 | }))); 1149 | } 1150 | 1151 | /** 1152 | * @name getIssueWatchers 1153 | * @function 1154 | * List all watchers for an issue 1155 | * [Jira Doc](http://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getIssueWatchers) 1156 | * @param {string} issueNumber - The issue number to search for including the project key 1157 | */ 1158 | getIssueWatchers(issueNumber) { 1159 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1160 | pathname: `/issue/${issueNumber}/watchers`, 1161 | }))); 1162 | } 1163 | 1164 | /** List all priorities jira knows about 1165 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1166 | * @name listPriorities 1167 | * @function 1168 | */ 1169 | listPriorities() { 1170 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1171 | pathname: '/priority', 1172 | }))); 1173 | } 1174 | 1175 | /** List Transitions for a specific issue that are available to the current user 1176 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1177 | * @name listTransitions 1178 | * @function 1179 | * @param {string} issueId - get transitions available for the issue 1180 | */ 1181 | listTransitions(issueId) { 1182 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1183 | pathname: `/issue/${issueId}/transitions`, 1184 | query: { 1185 | expand: 'transitions.fields', 1186 | }, 1187 | }))); 1188 | } 1189 | 1190 | /** Transition issue in Jira 1191 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1192 | * @name transitionsIssue 1193 | * @function 1194 | * @param {string} issueId - the Id of the issue to delete 1195 | * @param {object} issueTransition - transition object from the jira rest API 1196 | */ 1197 | transitionIssue(issueId, issueTransition) { 1198 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1199 | pathname: `/issue/${issueId}/transitions`, 1200 | }), { 1201 | body: issueTransition, 1202 | method: 'POST', 1203 | followAllRedirects: true, 1204 | })); 1205 | } 1206 | 1207 | /** List all Viewable Projects 1208 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id289193) 1209 | * @name listProjects 1210 | * @function 1211 | */ 1212 | listProjects() { 1213 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1214 | pathname: '/project', 1215 | }))); 1216 | } 1217 | 1218 | /** Add a comment to an issue 1219 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#id108798) 1220 | * @name addComment 1221 | * @function 1222 | * @param {string} issueId - Issue to add a comment to 1223 | * @param {string} comment - string containing comment 1224 | */ 1225 | addComment(issueId, comment) { 1226 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1227 | pathname: `/issue/${issueId}/comment`, 1228 | }), { 1229 | body: { 1230 | body: comment, 1231 | }, 1232 | method: 'POST', 1233 | followAllRedirects: true, 1234 | })); 1235 | } 1236 | 1237 | /** Add a comment to an issue, supports full comment object 1238 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#id108798) 1239 | * @name addCommentAdvanced 1240 | * @function 1241 | * @param {string} issueId - Issue to add a comment to 1242 | * @param {object} comment - The object containing your comment data 1243 | */ 1244 | addCommentAdvanced(issueId, comment) { 1245 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1246 | pathname: `/issue/${issueId}/comment`, 1247 | }), { 1248 | body: comment, 1249 | method: 'POST', 1250 | followAllRedirects: true, 1251 | })); 1252 | } 1253 | 1254 | /** Update comment for an issue 1255 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-updateComment) 1256 | * @name updateComment 1257 | * @function 1258 | * @param {string} issueId - Issue with the comment 1259 | * @param {string} commentId - Comment that is updated 1260 | * @param {string} comment - string containing new comment 1261 | * @param {object} [options={}] - extra options 1262 | */ 1263 | updateComment(issueId, commentId, comment, options = {}) { 1264 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1265 | pathname: `/issue/${issueId}/comment/${commentId}`, 1266 | }), { 1267 | body: { 1268 | body: comment, 1269 | ...options, 1270 | }, 1271 | method: 'PUT', 1272 | followAllRedirects: true, 1273 | })); 1274 | } 1275 | 1276 | /** 1277 | * @name getComments 1278 | * @function 1279 | * Get Comments by IssueId. 1280 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-comment-list-post) 1281 | * @param {string} issueId - this issue this comment is on 1282 | */ 1283 | getComments(issueId) { 1284 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1285 | pathname: `/issue/${issueId}/comment`, 1286 | }))); 1287 | } 1288 | 1289 | /** 1290 | * @name getComment 1291 | * @function 1292 | * Get Comment by Id. 1293 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-comment-list-post) 1294 | * @param {string} issueId - this issue this comment is on 1295 | * @param {number} commentId - the id of the comment 1296 | */ 1297 | getComment(issueId, commentId) { 1298 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1299 | pathname: `/issue/${issueId}/comment/${commentId}`, 1300 | }))); 1301 | } 1302 | 1303 | /** 1304 | * @name deleteComment 1305 | * @function 1306 | * Delete Comments by Id. 1307 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-comment-list-post) 1308 | * @param {string} issueId - this issue this comment is on 1309 | * @param {number} commentId - the id of the comment 1310 | */ 1311 | deleteComment(issueId, commentId) { 1312 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1313 | pathname: `/issue/${issueId}/comment/${commentId}`, 1314 | }), { 1315 | method: 'DELETE', 1316 | followAllRedirects: true, 1317 | })); 1318 | } 1319 | 1320 | /** Add a worklog to a project 1321 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id291617) 1322 | * @name addWorklog 1323 | * @function 1324 | * @param {string} issueId - Issue to add a worklog to 1325 | * @param {object} worklog - worklog object from the rest API 1326 | * @param {object} newEstimate - the new value for the remaining estimate field 1327 | * @param {object} [options={}] - extra options 1328 | */ 1329 | addWorklog(issueId, worklog, newEstimate = null, options = {}) { 1330 | const query = { 1331 | adjustEstimate: newEstimate ? 'new' : 'auto', 1332 | ...newEstimate ? { newEstimate } : {}, 1333 | ...options, 1334 | }; 1335 | 1336 | const header = { 1337 | uri: this.makeUri({ 1338 | pathname: `/issue/${issueId}/worklog`, 1339 | query, 1340 | }), 1341 | body: worklog, 1342 | method: 'POST', 1343 | 'Content-Type': 'application/json', 1344 | json: true, 1345 | }; 1346 | 1347 | return this.doRequest(header); 1348 | } 1349 | 1350 | /** Get ids of worklogs modified since 1351 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/worklog-getWorklogsForIds) 1352 | * @name updatedWorklogs 1353 | * @function 1354 | * @param {number} since - a date time in unix timestamp format since when updated worklogs 1355 | * will be returned. 1356 | * @param {string} expand - ptional comma separated list of parameters to expand: properties 1357 | * (provides worklog properties). 1358 | */ 1359 | updatedWorklogs(since, expand) { 1360 | const header = { 1361 | uri: this.makeUri({ 1362 | pathname: '/worklog/updated', 1363 | query: { since, expand }, 1364 | }), 1365 | method: 'GET', 1366 | 'Content-Type': 'application/json', 1367 | json: true, 1368 | }; 1369 | 1370 | return this.doRequest(header); 1371 | } 1372 | 1373 | /** Delete worklog from issue 1374 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#d2e1673) 1375 | * @name deleteWorklog 1376 | * @function 1377 | * @param {string} issueId - the Id of the issue to delete 1378 | * @param {string} worklogId - the Id of the worklog in issue to delete 1379 | */ 1380 | deleteWorklog(issueId, worklogId) { 1381 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1382 | pathname: `/issue/${issueId}/worklog/${worklogId}`, 1383 | }), { 1384 | method: 'DELETE', 1385 | followAllRedirects: true, 1386 | })); 1387 | } 1388 | 1389 | /** Update worklog from issue 1390 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-worklogs/#api-rest-api-2-issue-issueidorkey-worklog-id-put) 1391 | * @name updateWorklog 1392 | * @function 1393 | * @param {string} issueId - the Id of the issue to update 1394 | * @param {string} worklogId - the Id of the worklog in issue to update 1395 | * @param {string} body - value to set 1396 | */ 1397 | updateWorklog(issueId, worklogId, body) { 1398 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1399 | pathname: `/issue/${issueId}/worklog/${worklogId}`, 1400 | }), { 1401 | method: 'PUT', 1402 | body, 1403 | followAllRedirects: true, 1404 | })); 1405 | } 1406 | 1407 | /** Deletes an issue link. 1408 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-issueLink-linkId-delete) 1409 | * @name deleteIssueLink 1410 | * @function 1411 | * @param {string} linkId - the Id of the issue link to delete 1412 | */ 1413 | deleteIssueLink(linkId) { 1414 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1415 | pathname: `/issueLink/${linkId}`, 1416 | }), { 1417 | method: 'DELETE', 1418 | followAllRedirects: true, 1419 | })); 1420 | } 1421 | 1422 | /** Returns worklog details for a list of worklog IDs. 1423 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-worklog-list-post) 1424 | * @name getWorklogs 1425 | * @function 1426 | * @param {array} worklogsIDs - a list of worklog IDs. 1427 | * @param {string} expand - expand to include additional information about worklogs 1428 | * 1429 | */ 1430 | getWorklogs(worklogsIDs, expand) { 1431 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1432 | pathname: '/worklog/list', 1433 | query: { 1434 | expand, 1435 | }, 1436 | }), { 1437 | method: 'POST', 1438 | body: { 1439 | ids: worklogsIDs, 1440 | }, 1441 | })); 1442 | } 1443 | 1444 | /** Get worklogs list from a given issue 1445 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-issue-issueIdOrKey-worklog-get) 1446 | * @name getIssueWorklogs 1447 | * @function 1448 | * @param {string} issueId - the Id of the issue to find worklogs for 1449 | * @param {integer} [startAt=0] - optional starting index number 1450 | * @param {integer} [maxResults=1000] - optional ending index number 1451 | */ 1452 | getIssueWorklogs(issueId, startAt = 0, maxResults = 1000) { 1453 | return this.doRequest(this.makeRequestHeader( 1454 | this.makeUri({ 1455 | pathname: `/issue/${issueId}/worklog`, 1456 | query: { 1457 | startAt, 1458 | maxResults, 1459 | }, 1460 | }), 1461 | )); 1462 | } 1463 | 1464 | /** List all Issue Types jira knows about 1465 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id295946) 1466 | * @name listIssueTypes 1467 | * @function 1468 | */ 1469 | listIssueTypes() { 1470 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1471 | pathname: '/issuetype', 1472 | }))); 1473 | } 1474 | 1475 | /** Register a webhook 1476 | * [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1477 | * @name registerWebhook 1478 | * @function 1479 | * @param {object} webhook - properly formatted webhook 1480 | */ 1481 | registerWebhook(webhook) { 1482 | return this.doRequest(this.makeRequestHeader(this.makeWebhookUri({ 1483 | pathname: '/webhook', 1484 | }), { 1485 | method: 'POST', 1486 | body: webhook, 1487 | })); 1488 | } 1489 | 1490 | /** List all registered webhooks 1491 | * [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1492 | * @name listWebhooks 1493 | * @function 1494 | */ 1495 | listWebhooks() { 1496 | return this.doRequest(this.makeRequestHeader(this.makeWebhookUri({ 1497 | pathname: '/webhook', 1498 | }))); 1499 | } 1500 | 1501 | /** Get a webhook by its ID 1502 | * [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1503 | * @name getWebhook 1504 | * @function 1505 | * @param {string} webhookID - id of webhook to get 1506 | */ 1507 | getWebhook(webhookID) { 1508 | return this.doRequest(this.makeRequestHeader(this.makeWebhookUri({ 1509 | pathname: `/webhook/${webhookID}`, 1510 | }))); 1511 | } 1512 | 1513 | /** Delete a registered webhook 1514 | * [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1515 | * @name issueLink 1516 | * @function 1517 | * @param {string} webhookID - id of the webhook to delete 1518 | */ 1519 | deleteWebhook(webhookID) { 1520 | return this.doRequest(this.makeRequestHeader(this.makeWebhookUri({ 1521 | pathname: `/webhook/${webhookID}`, 1522 | }), { 1523 | method: 'DELETE', 1524 | })); 1525 | } 1526 | 1527 | /** Describe the currently authenticated user 1528 | * [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id2e865) 1529 | * @name getCurrentUser 1530 | * @function 1531 | */ 1532 | getCurrentUser() { 1533 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1534 | pathname: '/myself', 1535 | }))); 1536 | } 1537 | 1538 | /** Retrieve the backlog of a certain Rapid View 1539 | * @name getBacklogForRapidView 1540 | * @function 1541 | * @param {string} rapidViewId - rapid view id 1542 | */ 1543 | getBacklogForRapidView(rapidViewId) { 1544 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1545 | pathname: '/xboard/plan/backlog/data', 1546 | query: { 1547 | rapidViewId, 1548 | }, 1549 | }))); 1550 | } 1551 | 1552 | /** Add attachment to a Issue 1553 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#api/2/issue/{issueIdOrKey}/attachments-addAttachment) 1554 | * @name addAttachmentOnIssue 1555 | * @function 1556 | * @param {string} issueId - issue id 1557 | * @param {object} readStream - readStream object from fs 1558 | */ 1559 | addAttachmentOnIssue(issueId, readStream) { 1560 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1561 | pathname: `/issue/${issueId}/attachments`, 1562 | }), { 1563 | method: 'POST', 1564 | headers: { 1565 | 'X-Atlassian-Token': 'nocheck', 1566 | }, 1567 | formData: { 1568 | file: readStream, 1569 | }, 1570 | })); 1571 | } 1572 | 1573 | /** Notify people related to issue 1574 | * [Jira Doc](https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-notify) 1575 | * @name issueNotify 1576 | * @function 1577 | * @param {string} issueId - issue id 1578 | * @param {object} notificationBody - properly formatted body 1579 | */ 1580 | issueNotify(issueId, notificationBody) { 1581 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1582 | pathname: `/issue/${issueId}/notify`, 1583 | }), { 1584 | method: 'POST', 1585 | body: notificationBody, 1586 | })); 1587 | } 1588 | 1589 | /** Get list of possible statuses 1590 | * [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#api/2/status-getStatuses) 1591 | * @name listStatus 1592 | * @function 1593 | */ 1594 | listStatus() { 1595 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 1596 | pathname: '/status', 1597 | }))); 1598 | } 1599 | 1600 | /** Get a Dev-Status summary by issue ID 1601 | * @name getDevStatusSummary 1602 | * @function 1603 | * @param {string} issueId - id of issue to get 1604 | */ 1605 | getDevStatusSummary(issueId) { 1606 | return this.doRequest(this.makeRequestHeader(this.makeDevStatusUri({ 1607 | pathname: '/summary', 1608 | query: { 1609 | issueId, 1610 | }, 1611 | }))); 1612 | } 1613 | 1614 | /** Get a Dev-Status detail by issue ID 1615 | * @name getDevStatusDetail 1616 | * @function 1617 | * @param {string} issueId - id of issue to get 1618 | * @param {string} applicationType - type of application (stash, bitbucket) 1619 | * @param {string} dataType - info to return (repository, pullrequest) 1620 | */ 1621 | getDevStatusDetail(issueId, applicationType, dataType) { 1622 | return this.doRequest(this.makeRequestHeader(this.makeDevStatusUri({ 1623 | pathname: '/detail', 1624 | query: { 1625 | issueId, 1626 | applicationType, 1627 | dataType, 1628 | }, 1629 | }))); 1630 | } 1631 | 1632 | /** Get issue 1633 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/issue-getIssue) 1634 | * @name getIssue 1635 | * @function 1636 | * @param {string} issueIdOrKey - Id of issue 1637 | * @param {string} [fields] - The list of fields to return for each issue. 1638 | * @param {string} [expand] - A comma-separated list of the parameters to expand. 1639 | */ 1640 | getIssue(issueIdOrKey, fields, expand) { 1641 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1642 | pathname: `/issue/${issueIdOrKey}`, 1643 | query: { 1644 | fields, 1645 | expand, 1646 | }, 1647 | }))); 1648 | } 1649 | 1650 | /** Move issues to backlog 1651 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/backlog-moveIssuesToBacklog) 1652 | * @name moveToBacklog 1653 | * @function 1654 | * @param {array} issues - id or key of issues to get 1655 | */ 1656 | moveToBacklog(issues) { 1657 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1658 | pathname: '/backlog/issue', 1659 | }), { 1660 | method: 'POST', 1661 | body: { 1662 | issues, 1663 | }, 1664 | })); 1665 | } 1666 | 1667 | /** Get all boards 1668 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards) 1669 | * @name getAllBoards 1670 | * @function 1671 | * @param {number} [startAt=0] - The starting index of the returned boards. 1672 | * @param {number} [maxResults=50] - The maximum number of boards to return per page. 1673 | * @param {string} [type] - Filters results to boards of the specified type. 1674 | * @param {string} [name] - Filters results to boards that match the specified name. 1675 | * @param {string} [projectKeyOrId] - Filters results to boards that are relevant to a project. 1676 | */ 1677 | getAllBoards(startAt = 0, maxResults = 50, type, name, projectKeyOrId) { 1678 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1679 | pathname: '/board', 1680 | query: { 1681 | startAt, 1682 | maxResults, 1683 | type, 1684 | name, 1685 | ...projectKeyOrId && { projectKeyOrId }, 1686 | }, 1687 | }))); 1688 | } 1689 | 1690 | /** Create Board 1691 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard) 1692 | * @name createBoard 1693 | * @function 1694 | * @param {object} boardBody - Board name, type and filter Id is required. 1695 | * @param {string} boardBody.type - Valid values: scrum, kanban 1696 | * @param {string} boardBody.name - Must be less than 255 characters. 1697 | * @param {string} boardBody.filterId - Id of a filter that the user has permissions to view. 1698 | */ 1699 | createBoard(boardBody) { 1700 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1701 | pathname: '/board', 1702 | }), { 1703 | method: 'POST', 1704 | body: boardBody, 1705 | })); 1706 | } 1707 | 1708 | /** Get Board 1709 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard) 1710 | * @name getBoard 1711 | * @function 1712 | * @param {string} boardId - Id of board to retrieve 1713 | */ 1714 | getBoard(boardId) { 1715 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1716 | pathname: `/board/${boardId}`, 1717 | }))); 1718 | } 1719 | 1720 | /** Delete Board 1721 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard) 1722 | * @name deleteBoard 1723 | * @function 1724 | * @param {string} boardId - Id of board to retrieve 1725 | */ 1726 | deleteBoard(boardId) { 1727 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1728 | pathname: `/board/${boardId}`, 1729 | }), { 1730 | method: 'DELETE', 1731 | })); 1732 | } 1733 | 1734 | /** Get issues for backlog 1735 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getIssuesForBacklog) 1736 | * @name getIssuesForBacklog 1737 | * @function 1738 | * @param {string} boardId - Id of board to retrieve 1739 | * @param {number} [startAt=0] - The starting index of the returned issues. Base index: 0. 1740 | * @param {number} [maxResults=50] - The maximum number of issues to return per page. Default: 50. 1741 | * @param {string} [jql] - Filters results using a JQL query. 1742 | * @param {boolean} [validateQuery] - Specifies whether to validate the JQL query or not. 1743 | * Default: true. 1744 | * @param {string} [fields] - The list of fields to return for each issue. 1745 | */ 1746 | getIssuesForBacklog(boardId, startAt = 0, maxResults = 50, jql, validateQuery = true, fields) { 1747 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1748 | pathname: `/board/${boardId}/backlog`, 1749 | query: { 1750 | startAt, 1751 | maxResults, 1752 | jql, 1753 | validateQuery, 1754 | fields, 1755 | }, 1756 | }))); 1757 | } 1758 | 1759 | /** Get Configuration 1760 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getConfiguration) 1761 | * @name getConfiguration 1762 | * @function 1763 | * @param {string} boardId - Id of board to retrieve 1764 | */ 1765 | getConfiguration(boardId) { 1766 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1767 | pathname: `/board/${boardId}/configuration`, 1768 | }))); 1769 | } 1770 | 1771 | /** Get issues for board 1772 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getIssuesForBoard) 1773 | * @name getIssuesForBoard 1774 | * @function 1775 | * @param {string} boardId - Id of board to retrieve 1776 | * @param {number} [startAt=0] - The starting index of the returned issues. Base index: 0. 1777 | * @param {number} [maxResults=50] - The maximum number of issues to return per page. Default: 50. 1778 | * @param {string} [jql] - Filters results using a JQL query. 1779 | * @param {boolean} [validateQuery] - Specifies whether to validate the JQL query or not. 1780 | * Default: true. 1781 | * @param {string} [fields] - The list of fields to return for each issue. 1782 | */ 1783 | getIssuesForBoard(boardId, startAt = 0, maxResults = 50, jql, validateQuery = true, fields) { 1784 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1785 | pathname: `/board/${boardId}/issue`, 1786 | query: { 1787 | startAt, 1788 | maxResults, 1789 | jql, 1790 | validateQuery, 1791 | fields, 1792 | }, 1793 | }))); 1794 | } 1795 | 1796 | /** Get issue estimation for board 1797 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/issue-getIssueEstimationForBoard) 1798 | * @name getIssueEstimationForBoard 1799 | * @function 1800 | * @param {string} issueIdOrKey - Id of issue 1801 | * @param {number} boardId - The id of the board required to determine which field 1802 | * is used for estimation. 1803 | */ 1804 | getIssueEstimationForBoard(issueIdOrKey, boardId) { 1805 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1806 | pathname: `/issue/${issueIdOrKey}/estimation`, 1807 | query: { 1808 | boardId, 1809 | }, 1810 | }))); 1811 | } 1812 | 1813 | /** Get Epics 1814 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/epic-getEpics) 1815 | * @name getEpics 1816 | * @function 1817 | * @param {string} boardId - Id of board to retrieve 1818 | * @param {number} [startAt=0] - The starting index of the returned epics. Base index: 0. 1819 | * @param {number} [maxResults=50] - The maximum number of epics to return per page. Default: 50. 1820 | * @param {string} [done] - Filters results to epics that are either done or not done. 1821 | * Valid values: true, false. 1822 | */ 1823 | getEpics(boardId, startAt = 0, maxResults = 50, done) { 1824 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1825 | pathname: `/board/${boardId}/epic`, 1826 | query: { 1827 | startAt, 1828 | maxResults, 1829 | done, 1830 | }, 1831 | }))); 1832 | } 1833 | 1834 | /** Get board issues for epic 1835 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/epic-getIssuesForEpic) 1836 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/epic-getIssuesWithoutEpic) 1837 | * @name getBoardIssuesForEpic 1838 | * @function 1839 | * @param {string} boardId - Id of board to retrieve 1840 | * @param {string} epicId - Id of epic to retrieve, specify 'none' to get issues without an epic. 1841 | * @param {number} [startAt=0] - The starting index of the returned issues. Base index: 0. 1842 | * @param {number} [maxResults=50] - The maximum number of issues to return per page. Default: 50. 1843 | * @param {string} [jql] - Filters results using a JQL query. 1844 | * @param {boolean} [validateQuery] - Specifies whether to validate the JQL query or not. 1845 | * Default: true. 1846 | * @param {string} [fields] - The list of fields to return for each issue. 1847 | */ 1848 | getBoardIssuesForEpic( 1849 | boardId, 1850 | epicId, 1851 | startAt = 0, 1852 | maxResults = 50, 1853 | jql, 1854 | validateQuery = true, 1855 | fields, 1856 | ) { 1857 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1858 | pathname: `/board/${boardId}/epic/${epicId}/issue`, 1859 | query: { 1860 | startAt, 1861 | maxResults, 1862 | jql, 1863 | validateQuery, 1864 | fields, 1865 | }, 1866 | }))); 1867 | } 1868 | 1869 | /** Estimate issue for board 1870 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/issue-estimateIssueForBoard) 1871 | * @name estimateIssueForBoard 1872 | * @function 1873 | * @param {string} issueIdOrKey - Id of issue 1874 | * @param {number} boardId - The id of the board required to determine which field 1875 | * is used for estimation. 1876 | * @param {string} body - value to set 1877 | */ 1878 | estimateIssueForBoard(issueIdOrKey, boardId, body) { 1879 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1880 | pathname: `/issue/${issueIdOrKey}/estimation`, 1881 | query: { 1882 | boardId, 1883 | }, 1884 | }), { 1885 | method: 'PUT', 1886 | body, 1887 | })); 1888 | } 1889 | 1890 | /** Rank Issues 1891 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/issue-rankIssues) 1892 | * @name rankIssues 1893 | * @function 1894 | * @param {string} body - value to set 1895 | */ 1896 | rankIssues(body) { 1897 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1898 | pathname: '/issue/rank', 1899 | }), { 1900 | method: 'PUT', 1901 | body, 1902 | })); 1903 | } 1904 | 1905 | /** Get Projects 1906 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/project-getProjects) 1907 | * @name getProjects 1908 | * @function 1909 | * @param {string} boardId - Id of board to retrieve 1910 | * @param {number} [startAt=0] - The starting index of the returned projects. Base index: 0. 1911 | * @param {number} [maxResults=50] - The maximum number of projects to return per page. 1912 | * Default: 50. 1913 | */ 1914 | getProjects(boardId, startAt = 0, maxResults = 50) { 1915 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1916 | pathname: `/board/${boardId}/project`, 1917 | query: { 1918 | startAt, 1919 | maxResults, 1920 | }, 1921 | }))); 1922 | } 1923 | 1924 | /** Get Projects Full 1925 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/project-getProjectsFull) 1926 | * @name getProjectsFull 1927 | * @function 1928 | * @param {string} boardId - Id of board to retrieve 1929 | */ 1930 | getProjectsFull(boardId) { 1931 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1932 | pathname: `/board/${boardId}/project/full`, 1933 | }))); 1934 | } 1935 | 1936 | /** Get Board Properties Keys 1937 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/properties-getPropertiesKeys) 1938 | * @name getBoardPropertiesKeys 1939 | * @function 1940 | * @param {string} boardId - Id of board to retrieve 1941 | */ 1942 | getBoardPropertiesKeys(boardId) { 1943 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1944 | pathname: `/board/${boardId}/properties`, 1945 | }))); 1946 | } 1947 | 1948 | /** Delete Board Property 1949 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/properties-deleteProperty) 1950 | * @name deleteBoardProperty 1951 | * @function 1952 | * @param {string} boardId - Id of board to retrieve 1953 | * @param {string} propertyKey - Id of property to delete 1954 | */ 1955 | deleteBoardProperty(boardId, propertyKey) { 1956 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1957 | pathname: `/board/${boardId}/properties/${propertyKey}`, 1958 | }), { 1959 | method: 'DELETE', 1960 | })); 1961 | } 1962 | 1963 | /** Set Board Property 1964 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/properties-setProperty) 1965 | * @name setBoardProperty 1966 | * @function 1967 | * @param {string} boardId - Id of board to retrieve 1968 | * @param {string} propertyKey - Id of property to delete 1969 | * @param {string} body - value to set, for objects make sure to stringify first 1970 | */ 1971 | setBoardProperty(boardId, propertyKey, body) { 1972 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1973 | pathname: `/board/${boardId}/properties/${propertyKey}`, 1974 | }), { 1975 | method: 'PUT', 1976 | body, 1977 | })); 1978 | } 1979 | 1980 | /** Get Board Property 1981 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/properties-getProperty) 1982 | * @name getBoardProperty 1983 | * @function 1984 | * @param {string} boardId - Id of board to retrieve 1985 | * @param {string} propertyKey - Id of property to retrieve 1986 | */ 1987 | getBoardProperty(boardId, propertyKey) { 1988 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 1989 | pathname: `/board/${boardId}/properties/${propertyKey}`, 1990 | }))); 1991 | } 1992 | 1993 | /** Get All Sprints 1994 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint-getAllSprints) 1995 | * @name getAllSprints 1996 | * @function 1997 | * @param {string} boardId - Id of board to retrieve 1998 | * @param {number} [startAt=0] - The starting index of the returned sprints. Base index: 0. 1999 | * @param {number} [maxResults=50] - The maximum number of sprints to return per page. 2000 | * Default: 50. 2001 | * @param {string} [state] - Filters results to sprints in specified states. 2002 | * Valid values: future, active, closed. 2003 | */ 2004 | getAllSprints(boardId, startAt = 0, maxResults = 50, state) { 2005 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2006 | pathname: `/board/${boardId}/sprint`, 2007 | query: { 2008 | startAt, 2009 | maxResults, 2010 | state, 2011 | }, 2012 | }))); 2013 | } 2014 | 2015 | /** Get Board issues for sprint 2016 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-agile-1-0-board-boardid-sprint-sprintid-issue-get) 2017 | * @name getBoardIssuesForSprint 2018 | * @function 2019 | * @param {string} boardId - Id of board to retrieve 2020 | * @param {string} sprintId - Id of sprint to retrieve 2021 | * @param {number} [startAt=0] - The starting index of the returned issues. Base index: 0. 2022 | * @param {number} [maxResults=50] - The maximum number of issues to return per page. Default: 50. 2023 | * @param {string} [jql] - Filters results using a JQL query. 2024 | * @param {boolean} [validateQuery] - Specifies whether to validate the JQL query or not. 2025 | * Default: true. 2026 | * @param {string} [fields] - The list of fields to return for each issue. 2027 | * @param {string} [expand] - A comma-separated list of the parameters to expand. 2028 | */ 2029 | getBoardIssuesForSprint( 2030 | boardId, 2031 | sprintId, 2032 | startAt = 0, 2033 | maxResults = 50, 2034 | jql, 2035 | validateQuery = true, 2036 | fields, 2037 | expand, 2038 | ) { 2039 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2040 | pathname: `/board/${boardId}/sprint/${sprintId}/issue`, 2041 | query: { 2042 | startAt, 2043 | maxResults, 2044 | jql, 2045 | validateQuery, 2046 | fields, 2047 | expand, 2048 | }, 2049 | }))); 2050 | } 2051 | 2052 | /** Get All Versions 2053 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/version-getAllVersions) 2054 | * @name getAllVersions 2055 | * @function 2056 | * @param {string} boardId - Id of board to retrieve 2057 | * @param {number} [startAt=0] - The starting index of the returned versions. Base index: 0. 2058 | * @param {number} [maxResults=50] - The maximum number of versions to return per page. 2059 | * Default: 50. 2060 | * @param {string} [released] - Filters results to versions that are either released or 2061 | * unreleased.Valid values: true, false. 2062 | */ 2063 | getAllVersions(boardId, startAt = 0, maxResults = 50, released) { 2064 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2065 | pathname: `/board/${boardId}/version`, 2066 | query: { 2067 | startAt, 2068 | maxResults, 2069 | released, 2070 | }, 2071 | }))); 2072 | } 2073 | 2074 | /** Get Filter 2075 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/filter) 2076 | * @name getFilter 2077 | * @function 2078 | * @param {string} filterId - Id of filter to retrieve 2079 | */ 2080 | 2081 | getFilter(filterId) { 2082 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2083 | pathname: `/filter/${filterId}`, 2084 | }))); 2085 | } 2086 | 2087 | /** Get Epic 2088 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/epic-getEpic) 2089 | * @name getEpic 2090 | * @function 2091 | * @param {string} epicIdOrKey - Id of epic to retrieve 2092 | */ 2093 | getEpic(epicIdOrKey) { 2094 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2095 | pathname: `/epic/${epicIdOrKey}`, 2096 | }))); 2097 | } 2098 | 2099 | /** Partially update epic 2100 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/epic-partiallyUpdateEpic) 2101 | * @name partiallyUpdateEpic 2102 | * @function 2103 | * @param {string} epicIdOrKey - Id of epic to retrieve 2104 | * @param {string} body - value to set, for objects make sure to stringify first 2105 | */ 2106 | partiallyUpdateEpic(epicIdOrKey, body) { 2107 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2108 | pathname: `/epic/${epicIdOrKey}`, 2109 | }), { 2110 | method: 'POST', 2111 | body, 2112 | })); 2113 | } 2114 | 2115 | /** Get issues for epic 2116 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/epic-getIssuesForEpic) 2117 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/epic-getIssuesWithoutEpic) 2118 | * @name getIssuesForEpic 2119 | * @function 2120 | * @param {string} epicId - Id of epic to retrieve, specify 'none' to get issues without an epic. 2121 | * @param {number} [startAt=0] - The starting index of the returned issues. Base index: 0. 2122 | * @param {number} [maxResults=50] - The maximum number of issues to return per page. Default: 50. 2123 | * @param {string} [jql] - Filters results using a JQL query. 2124 | * @param {boolean} [validateQuery] - Specifies whether to validate the JQL query or not. 2125 | * Default: true. 2126 | * @param {string} [fields] - The list of fields to return for each issue. 2127 | */ 2128 | getIssuesForEpic( 2129 | epicId, 2130 | startAt = 0, 2131 | maxResults = 50, 2132 | jql, 2133 | validateQuery = true, 2134 | fields, 2135 | ) { 2136 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2137 | pathname: `/epic/${epicId}/issue`, 2138 | query: { 2139 | startAt, 2140 | maxResults, 2141 | jql, 2142 | validateQuery, 2143 | fields, 2144 | }, 2145 | }))); 2146 | } 2147 | 2148 | /** Move Issues to Epic 2149 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/epic-moveIssuesToEpic) 2150 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/epic-removeIssuesFromEpic) 2151 | * @name moveIssuesToEpic 2152 | * @function 2153 | * @param {string} epicIdOrKey - Id of epic to move issue to, or 'none' to remove from epic 2154 | * @param {array} issues - array of issues to move 2155 | */ 2156 | moveIssuesToEpic(epicIdOrKey, issues) { 2157 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2158 | pathname: `/epic/${epicIdOrKey}/issue`, 2159 | }), { 2160 | method: 'POST', 2161 | body: { 2162 | issues, 2163 | }, 2164 | })); 2165 | } 2166 | 2167 | /** Rank Epics 2168 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/epic-rankEpics) 2169 | * @name rankEpics 2170 | * @function 2171 | * @param {string} epicIdOrKey - Id of epic 2172 | * @param {string} body - value to set 2173 | */ 2174 | rankEpics(epicIdOrKey, body) { 2175 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2176 | pathname: `/epic/${epicIdOrKey}/rank`, 2177 | }), { 2178 | method: 'PUT', 2179 | body, 2180 | })); 2181 | } 2182 | 2183 | /** 2184 | * @name getServerInfo 2185 | * @function 2186 | * Get server info 2187 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-api-2-serverInfo-get) 2188 | */ 2189 | getServerInfo() { 2190 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 2191 | pathname: '/serverInfo', 2192 | }))); 2193 | } 2194 | 2195 | /** 2196 | * @name getIssueCreateMetadata 2197 | * @param {object} optional - object containing any of the following properties 2198 | * @param {array} [optional.projectIds]: optional Array of project ids to return metadata for 2199 | * @param {array} [optional.projectKeys]: optional Array of project keys to return metadata for 2200 | * @param {array} [optional.issuetypeIds]: optional Array of issuetype ids to return metadata for 2201 | * @param {array} [optional.issuetypeNames]: optional Array of issuetype names to return metadata 2202 | * for 2203 | * @param {string} [optional.expand]: optional Include additional information about issue 2204 | * metadata. Valid value is 'projects.issuetypes.fields' 2205 | * Get metadata for creating an issue. 2206 | * [Jira Doc](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-createmeta-get) 2207 | */ 2208 | getIssueCreateMetadata(optional = {}) { 2209 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 2210 | pathname: '/issue/createmeta', 2211 | query: optional, 2212 | }))); 2213 | } 2214 | 2215 | /** Generic Get Request 2216 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/2/) 2217 | * @name genericGet 2218 | * @function 2219 | * @param {string} endpoint - Rest API endpoint 2220 | */ 2221 | genericGet(endpoint) { 2222 | return this.doRequest(this.makeRequestHeader(this.makeUri({ 2223 | pathname: `/${endpoint}`, 2224 | }))); 2225 | } 2226 | 2227 | /** Generic Get Request to the Agile API 2228 | * [Jira Doc](https://docs.atlassian.com/jira-software/REST/cloud/2/) 2229 | * @name genericGet 2230 | * @function 2231 | * @param {string} endpoint - Rest API endpoint 2232 | */ 2233 | genericAgileGet(endpoint) { 2234 | return this.doRequest(this.makeRequestHeader(this.makeAgileUri({ 2235 | pathname: `/${endpoint}`, 2236 | }))); 2237 | } 2238 | } 2239 | --------------------------------------------------------------------------------