Skip to content

Commit

Permalink
feature: Add early termination to paginate method (#329)
Browse files Browse the repository at this point in the history
  • Loading branch information
rtsao authored and JasonEtco committed Jan 11, 2018
1 parent 6b9c41d commit 36e3b57
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 8 deletions.
51 changes: 46 additions & 5 deletions docs/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,52 @@ Many GitHub API endpoints are paginated. The `github.paginate` method can be use
```js
module.exports = robot => {
robot.on('issues.opened', context => {
context.github.paginate(context.github.issues.getAll(context.repo()), res => {
res.data.issues.forEach(issue => {
context.log('Issue: %s', issue.title)
})
})
context.github.paginate(
context.github.issues.getAll(context.repo()),
res => {
res.data.issues.forEach(issue => {
context.log('Issue: %s', issue.title)
})
}
)
})
}
```

## Accumulating pages

The return value of the `github.paginate` callback will be used to accumulate results.

```js
module.exports = robot => {
robot.on('issues.opened', async context => {
const allIssues = await context.github.paginate(
context.github.issues.getAll(context.repo()),
res => res.data
)
console.log(allIssues)
})
}
```

## Early exit

Sometimes it is desirable to stop fetching pages after a certain condition has been satisfied. A second argument, `done`, is provided to the callback and can be used to stop pagination. After `done` is invoked, no additional pages will be fetched.

```js
module.exports = robot => {
robot.on('issues.opened', context => {
context.github.paginate(
context.github.issues.getAll(context.repo()),
(res, done) => {
for (let issue of res.data) {
if (issue.body.includes('something')) {
console.log('found it:', issue)
done()
}
}
}
)
})
}
```
11 changes: 8 additions & 3 deletions lib/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ class EnhancedGitHubClient extends GitHubApi {

async paginate (responsePromise, callback = defaultCallback) {
let collection = []
let getNextPage = true
let done = () => {
getNextPage = false
}
let response = await responsePromise
collection = collection.concat(await callback(response))
while (this.hasNextPage(response)) {
collection = collection.concat(await callback(response, done))
// eslint-disable-next-line no-unmodified-loop-condition
while (getNextPage && this.hasNextPage(response)) {
response = await this.getNextPage(response)
collection = collection.concat(await callback(response))
collection = collection.concat(await callback(response, done))
}
return collection
}
Expand Down
66 changes: 66 additions & 0 deletions test/github.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const EnhancedGitHubClient = require('../lib/github')
const nock = require('nock')
const Bottleneck = require('bottleneck')

describe('EnhancedGitHubClient', () => {
let github

beforeEach(() => {
const logger = {
debug: jest.fn(),
trace: jest.fn()
}

github = new EnhancedGitHubClient({ logger })

// Set a shorter limiter, otherwise tests are _slow_
github.limiter = new Bottleneck(1, 1)
})

describe('paginate', () => {
beforeEach(() => {
// Prepare an array of issue objects
const issues = new Array(5).fill().map((_, i, arr) => {
return {
title: `Issue number ${i}`,
id: i,
number: i
}
})

nock('https://api.github.com')
.get('/repos/JasonEtco/pizza/issues?per_page=1').reply(200, issues[0], {
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=2>; rel="next"'
})
.get('/repositories/123/issues?per_page=1&page=2').reply(200, issues[1], {
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=3>; rel="next"'
})
.get('/repositories/123/issues?per_page=1&page=3').reply(200, issues[2], {
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=4>; rel="next"'
})
.get('/repositories/123/issues?per_page=1&page=4').reply(200, issues[3], {
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=5>; rel="next"'
})
.get('/repositories/123/issues?per_page=1&page=5').reply(200, issues[4], {
link: ''
})
})

it('returns an array of pages', async () => {
const spy = jest.fn()
const res = await github.paginate(github.issues.getForRepo({ owner: 'JasonEtco', repo: 'pizza', per_page: 1 }), spy)
expect(Array.isArray(res)).toBeTruthy()
expect(res.length).toBe(5)
expect(spy).toHaveBeenCalledTimes(5)
})

it('stops iterating if the done() function is called in the callback', async () => {
const spy = jest.fn((res, done) => {
if (res.data.id === 2) done()
})
const res = await github.paginate(github.issues.getForRepo({ owner: 'JasonEtco', repo: 'pizza', per_page: 1 }), spy)
expect(res.length).toBe(3)
expect(spy).toHaveBeenCalledTimes(3)
})
})
})

0 comments on commit 36e3b57

Please sign in to comment.