Skip to content

Commit

Permalink
Merge pull request #2 from Vali0/feature/PullRequests-Search-Query-En…
Browse files Browse the repository at this point in the history
…hancement

Feature/pull requests search query enhancement
  • Loading branch information
Vali0 committed Jun 21, 2018
2 parents f1b61bc + bf4e26a commit 8de8389
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 13 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,15 @@ client.refreshTokens()
```

## PullRequests
### getPullRequests(params, callback)
### getPullRequests(options, callback)
Sends request to Bitbucket API in order to get all pull requests by given parameters. Returns a promise.
- `params` - Query string parameters for get request. Based on [Bitbucket documentation](https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/pullrequests)
- `callback` - Usually used as a callback parameter for recursion if there is more than one page of pull requests
- `options`
- `q (optional)` - Search query for get request based on [Bitbucket documentation](https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/pullrequests). If present search will be performed for this query. If missing at least one of the following fields is required and query will be build internally with `AND` operator: state, updatedOn, destinationBranch
- `state (required if q is missing)` - Pull request state: MERGED, SUPERSEDED, OPEN, DECLINED
- `updatedOn (optional)` - an unquoted ISO-8601 date time string with the timezone offset, milliseconds and entire time component being optional
- `destinationBranch (optional)` - destination branch name
- `page (optional)` - page number if you want to skip pages
- `pullRequestsList` - Previously stored pull requests. Used in recursion when there is next page or if you want to append an already existing list to newly pulled one

Parameters must be an object with values based on Bitbucket API guidelines. In case of request failure because expired access token automatically calls `refreshTokens` from above and tries to refresh tokens. In case of success to refresh access token executes again `getPullRequests` with the same parameters. In case of failure to refresh access token throws an exception.

Expand Down Expand Up @@ -197,4 +202,6 @@ pullRequests.getPullRequests({

# Known issues
* All TODO across the code
* Tokens do not update automatically in config json
* Tokens do not update automatically in config json
* Pull requests query to be build with different than AND operator
* Ability to pass standard date time or string to getPullRequests and internally convert them to ISO-8601 format
63 changes: 55 additions & 8 deletions lib/PullRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,28 @@ class PullRequests {
/*
* Gets all pull requests
*
* @param {Object} parameters - Query parameters for GET request
* @param {Object} options - Search request parameters
* @param {Object} pullRequestsList - Previously stored pull requests. Used in recursion when there is next page
* @returns {Object|function} - serializedPullRequests if there is no next page return pull requests | this.getPullRequests if there is next page
*/
getPullRequests(parameters, pullRequestsList) {
getPullRequests(options, pullRequestsList) {
let params = {};

if (options === undefined || !Object.keys(options).length) {
throw new Error('Missing search parameters');
}

params.q = options.q || buildSearchQuery(options);
params.page = options.page || 1;

let pullRequestsObj = pullRequestsList || {}; // Checks if there are pull requests from previous recursion. If there are takes them if not - creates new empty object
let body = {
method: 'GET',
uri: this.pullRequestsUrl,
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
qs: parameters,
qs: params,
auth: {
bearer: this.bitbucket.accessToken
}
Expand All @@ -73,14 +82,13 @@ class PullRequests {
.then(data => {
try {
let pullRequests = JSON.parse(data); // Parses pull requests from API response
let serializedPullRequests = serializePullRequestsData.call(this, pullRequests.values, pullRequestsList);
let serializedPullRequests = serializePullRequestsData.call(this, pullRequests.values, pullRequestsObj);

// If there is next page make a recursion
if (pullRequests.next) {
parameters.page = parameters.page || 1;
parameters.page++;
options.page = ++params.page;

return this.getPullRequests(parameters, serializedPullRequests);
return this.getPullRequests(options, serializedPullRequests);
}

return serializedPullRequests;
Expand All @@ -102,7 +110,7 @@ class PullRequests {
// requires a return because it is a promise
return this.bitbucket.refreshTokens()
.then(() => {
return this.getPullRequests(parameters, pullRequestsObj);
return this.getPullRequests(options, pullRequestsObj);
})
.catch(err => {
throw new Error('PullRequests: Can not refresh access token. Stack trace: ' + err);
Expand All @@ -111,6 +119,13 @@ class PullRequests {
}
}

/*
* Serialzies pull request data in required format for future use
*
* @params {Object} pullRequests - Pull requests from Bitbucket response
* @params {Object} targetPullRequests - Previously serialzied pull requests
* @returns {Object} result - Serialized pull requests
*/
function serializePullRequestsData(pullRequests, targetPullRequests) {
let result = Object.assign({}, targetPullRequests); // Cloning target object so we don't change it by reference

Expand Down Expand Up @@ -151,4 +166,36 @@ function serializePullRequestsData(pullRequests, targetPullRequests) {
return result;
}

/*
* Builds request search query
*
* @params {String} params - Parameter for search query
* @returns {String} queryString - Generated search query
*/
function buildSearchQuery(params) {
let queryString = '';

if (params.state) {
queryString += 'state="' + params.state + '"';
}

if (params.updatedOn) {
if (queryString) {
queryString += ' AND updated_on >= ' + params.updatedOn;
} else {
queryString += 'updated_on >= ' + params.updatedOn;
}
}

if (params.destinationBranch) {
if (queryString) {
queryString += ' AND destination.branch.name="' + params.destinationBranch + '"';
} else {
queryString += 'destination.branch.name="' + params.destinationBranch + '"';
}
}

return queryString;
}

module.exports = PullRequests;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bitbucket-notifications",
"version": "1.2.0",
"version": "1.2.1",
"description": "Node.js application which can send an email with links to all PRs that have been merged in last 24 hours. It connects to Bitbucket, Gmail and Jira with OAuth2 for higher security by simply adding your credentials in configuration file.",
"main": "index.js",
"scripts": {
Expand Down
126 changes: 126 additions & 0 deletions test/PullRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,132 @@ describe('PullRequests', function() {
});
});

it('should throw an error if options are missing', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = function() {
return pullRequests.getPullRequests();
};

// assert
expect(pullRequestsData).to.throw('Missing search parameters');
});

it('should throw an error if options are passed but empty', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = function() {
return pullRequests.getPullRequests({});
};

// assert
expect(pullRequestsData).to.throw('Missing search parameters');
});

it('should use user search query instead of building new one', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = pullRequests.getPullRequests({
q: 'state="MERGED"',
state: 'OPEN'
});

// assert
expect(promise.getCall(0).args[0].qs).to.eql({
q: 'state="MERGED"',
page: 1
});
});

it('should build new search query if q is missing', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = pullRequests.getPullRequests({
state: 'OPEN'
});

// assert
expect(promise.getCall(0).args[0].qs).to.eql({
q: 'state="OPEN"',
page: 1
});
});

it('should build new search query with updated on parameter', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = pullRequests.getPullRequests({
updatedOn: '6-06-6006'
});

// assert
expect(promise.getCall(0).args[0].qs).to.eql({
q: 'updated_on >= 6-06-6006',
page: 1
});
});

it('should build new search query with destination branch name parameter', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = pullRequests.getPullRequests({
destinationBranch: 'foobar'
});

// assert
expect(promise.getCall(0).args[0].qs).to.eql({
q: 'destination.branch.name="foobar"',
page: 1
});
});

it('should append updatedOn parameter to state', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = pullRequests.getPullRequests({
state: 'MERGED',
updatedOn: '6-06-6006'
});

// assert
expect(promise.getCall(0).args[0].qs).to.eql({
q: 'state="MERGED" AND updated_on >= 6-06-6006',
page: 1
});
});

it('should append branch name parameter to state and updatedOn', function() {
// arrange

// act
let pullRequests = new PullRequests(bitbucket, username, repoSlug);
let pullRequestsData = pullRequests.getPullRequests({
state: 'MERGED',
updatedOn: '6-06-6006',
destinationBranch: 'foobar'
});

// assert
expect(promise.getCall(0).args[0].qs).to.eql({
q: 'state="MERGED" AND updated_on >= 6-06-6006 AND destination.branch.name="foobar"',
page: 1
});
});

it('should throw an error if response is rejected and can not refresh tokens', function() {
// arrange
promise.rejects('bad request');
Expand Down

0 comments on commit 8de8389

Please sign in to comment.