Skip to content

Commit

Permalink
Alternative implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jetzhou committed Feb 15, 2024
1 parent 5045792 commit 807b43c
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 36 deletions.
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,45 @@ await octokit.graphql.paginate(
);
```

### Options
### Early stop when iterating

You can provide a third argument to `paginate` or `iterator` to modify the behavior of the pagination.
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.

`maxPages` will stop the iteration at the specified number of pages, useful when you don't need all the items in the response but still want to take advantage of the automatic merging.
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();
}
},
);
```
const { repository } = await octokit.graphql.paginate(
`query paginate($cursor: String) {

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 {
Expand All @@ -178,8 +208,12 @@ const { repository } = await octokit.graphql.paginate(
}
}
}`,
{ },
{ maxPages: 10 },
{},
(response, done) => {
if (response?.repository?.issues?.nodes?.[0].title === "Issue 2") {
done();
}
},
);
```
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Octokit } from "@octokit/core";
import { createIterator } from "./iterator";
import { createPaginate } from "./paginate";
export type { PageInfoForward, PageInfoBackward } from "./page-info";
export type { Options } from "./options";

export function paginateGraphql(octokit: Octokit) {
octokit.graphql;
Expand Down
13 changes: 5 additions & 8 deletions src/iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,31 @@ import { extractPageInfos } from "./extract-page-info";
import { Octokit } from "@octokit/core";
import { getCursorFrom, hasAnotherPage } from "./page-info";
import { MissingCursorChange } from "./errors";
import type { Options } from "./options";

const createIterator = (octokit: Octokit) => {
return <ResponseType = any>(
query: string,
initialParameters: Record<string, any> = {},
options: Options = {},
stopFunction?: (response: ResponseType, done: () => void) => void,
) => {
let nextPageExists = true;
let stopEarly = false;
let parameters = { ...initialParameters };
const { maxPages } = options;
let page = 0;

return {
[Symbol.asyncIterator]: () => ({
async next() {
if (!nextPageExists) return { done: true, value: {} as ResponseType };
if (maxPages && page >= maxPages) {
if (!nextPageExists || stopEarly) {
return { done: true, value: {} as ResponseType };
}

page += 1;

const response = await octokit.graphql<ResponseType>(
query,
parameters,
);

stopFunction?.(response, () => (stopEarly = true));

const pageInfoContext = extractPageInfos(response);
const nextCursorValue = getCursorFrom(pageInfoContext.pageInfo);
nextPageExists = hasAnotherPage(pageInfoContext.pageInfo);
Expand Down
3 changes: 0 additions & 3 deletions src/options.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/paginate.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Octokit } from "@octokit/core";
import { mergeResponses } from "./merge-responses";
import { createIterator } from "./iterator";
import type { Options } from "./options";

const createPaginate = (octokit: Octokit) => {
const iterator = createIterator(octokit);
return async <ResponseType extends object = any>(
query: string,
initialParameters: Record<string, any> = {},
options: Options = {},
stopFunction?: (response: ResponseType, done: () => void) => void,
): Promise<ResponseType> => {
let mergedResponse: ResponseType = {} as ResponseType;
for await (const response of iterator<ResponseType>(
query,
initialParameters,
options,
stopFunction,
)) {
mergedResponse = mergeResponses<ResponseType>(mergedResponse, response);
}
Expand Down
77 changes: 63 additions & 14 deletions test/paginate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,30 +282,79 @@ describe("pagination", () => {
]);
});

it(".paginate.iterator() allows users to pass `maxPages` parameter and stops at the right place.", async (): Promise<void> => {
it(".paginate.iterator() allows users to pass `stopFunction` and stops at the right place.", async (): Promise<void> => {
const responses = createResponsePages({ amount: 3 });

const { octokit, getCallCount, getPassedVariablesForCall } = MockOctokit({
responses,
});

const maxPages = 2;
let pages = 0;

const actualResponse = await octokit.graphql.paginate<TestResponseType>(
`
query paginate ($cursor: String) {
repository(owner: "octokit", name: "rest.js") {
issues(first: 10, after: $cursor) {
nodes {
title
}
pageInfo {
hasNextPage
endCursor
}
`query paginate ($cursor: String) {
repository(owner: "octokit", name: "rest.js") {
issues(first: 10, after: $cursor) {
nodes {
title
}
pageInfo {
hasNextPage
endCursor
}
}
}`,
}
}`,
{},
{ maxPages: 2 },
(_, 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<void> => {
const responses = createResponsePages({ amount: 3 });

const { octokit, getCallCount, getPassedVariablesForCall } = MockOctokit({
responses,
});

const actualResponse = await octokit.graphql.paginate<TestResponseType>(
`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({
Expand Down

0 comments on commit 807b43c

Please sign in to comment.