diff --git a/README.md b/README.md index 6fc6e6b..c51d26b 100644 --- a/README.md +++ b/README.md @@ -157,9 +157,69 @@ await octokit.graphql.paginate( ); ``` +### Early stop when iterating + +You can provide a third argument, a function, to `paginate` or `iterator` to stop the pagination earlier. The function will be called with two arguments, the first is the content of the most recent page, the second is a `done` function to stop the iteration. + +For example, you can stop the iteration after a certain number of pages: + +```js +const maxPages = 2; +let pages = 0; + +await octokit.graphql.paginate( + `query paginate ($cursor: String) { + repository(owner: "octokit", name: "rest.js") { + issues(first: 10, after: $cursor) { + nodes { + title + } + pageInfo { + hasNextPage + endCursor + } + } + } + }`, + {}, + (_, done) => { + pages += 1; + if (pages >= maxPages) { + done(); + } + }, +); +``` + +Or, to stop after you find a certain item: + +```js +await octokit.graphql.paginate( + `query paginate ($cursor: String) { + repository(owner: "octokit", name: "rest.js") { + issues(first: 10, after: $cursor) { + nodes { + title + } + pageInfo { + hasNextPage + endCursor + } + } + } + }`, + {}, + (response, done) => { + if (response?.repository?.issues?.nodes?.[0].title === "Issue 2") { + done(); + } + }, +); +``` + ### Pagination Direction -You can control the pagination direction by the properties deinfed in the `pageInfo` resource. +You can control the pagination direction by the properties defined in the `pageInfo` resource. For a forward pagination, use: diff --git a/src/iterator.ts b/src/iterator.ts index fdd9f4b..1cbce30 100644 --- a/src/iterator.ts +++ b/src/iterator.ts @@ -7,20 +7,26 @@ const createIterator = (octokit: Octokit) => { return ( query: string, initialParameters: Record = {}, + stopFunction?: (response: ResponseType, done: () => void) => void, ) => { let nextPageExists = true; + let stopEarly = false; let parameters = { ...initialParameters }; return { [Symbol.asyncIterator]: () => ({ async next() { - if (!nextPageExists) return { done: true, value: {} as ResponseType }; + if (!nextPageExists || stopEarly) { + return { done: true, value: {} as ResponseType }; + } const response = await octokit.graphql( query, parameters, ); + stopFunction?.(response, () => (stopEarly = true)); + const pageInfoContext = extractPageInfos(response); const nextCursorValue = getCursorFrom(pageInfoContext.pageInfo); nextPageExists = hasAnotherPage(pageInfoContext.pageInfo); diff --git a/src/paginate.ts b/src/paginate.ts index 5c4f930..d9284ad 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -7,11 +7,13 @@ const createPaginate = (octokit: Octokit) => { return async ( query: string, initialParameters: Record = {}, + stopFunction?: (response: ResponseType, done: () => void) => void, ): Promise => { let mergedResponse: ResponseType = {} as ResponseType; for await (const response of iterator( query, initialParameters, + stopFunction, )) { mergedResponse = mergeResponses(mergedResponse, response); } diff --git a/test/paginate.test.ts b/test/paginate.test.ts index 0628cc0..8ffe05c 100644 --- a/test/paginate.test.ts +++ b/test/paginate.test.ts @@ -282,6 +282,94 @@ describe("pagination", () => { ]); }); + it(".paginate.iterator() allows users to pass `stopFunction` and stops at the right place.", async (): Promise => { + const responses = createResponsePages({ amount: 3 }); + + const { octokit, getCallCount, getPassedVariablesForCall } = MockOctokit({ + responses, + }); + + const maxPages = 2; + let pages = 0; + + const actualResponse = await octokit.graphql.paginate( + `query paginate ($cursor: String) { + repository(owner: "octokit", name: "rest.js") { + issues(first: 10, after: $cursor) { + nodes { + title + } + pageInfo { + hasNextPage + endCursor + } + } + } + }`, + {}, + (_, done) => { + pages += 1; + if (pages >= maxPages) { + done(); + } + }, + ); + + expect(actualResponse).toEqual({ + repository: { + issues: { + nodes: [{ title: "Issue 1" }, { title: "Issue 2" }], + pageInfo: { hasNextPage: true, endCursor: "endCursor2" }, + }, + }, + }); + expect(getCallCount()).toBe(2); + expect(getPassedVariablesForCall(1)).toBeUndefined(); + expect(getPassedVariablesForCall(2)).toEqual({ cursor: "endCursor1" }); + }); + + it(".paginate.iterator() allows users to pass `stopFunction` and stops at the right place.", async (): Promise => { + const responses = createResponsePages({ amount: 3 }); + + const { octokit, getCallCount, getPassedVariablesForCall } = MockOctokit({ + responses, + }); + + const actualResponse = await octokit.graphql.paginate( + `query paginate ($cursor: String) { + repository(owner: "octokit", name: "rest.js") { + issues(first: 10, after: $cursor) { + nodes { + title + } + pageInfo { + hasNextPage + endCursor + } + } + } + }`, + {}, + (response, done) => { + if (response?.repository?.issues?.nodes?.[0].title === "Issue 2") { + done(); + } + }, + ); + + expect(actualResponse).toEqual({ + repository: { + issues: { + nodes: [{ title: "Issue 1" }, { title: "Issue 2" }], + pageInfo: { hasNextPage: true, endCursor: "endCursor2" }, + }, + }, + }); + expect(getCallCount()).toBe(2); + expect(getPassedVariablesForCall(1)).toBeUndefined(); + expect(getPassedVariablesForCall(2)).toEqual({ cursor: "endCursor1" }); + }); + it("paginate() throws error with path and variable name if cursors do not change between calls.", async (): Promise => { const [responsePage1, responsePage2] = createResponsePages({ amount: 2 }); responsePage2.repository.issues.pageInfo = {