@openally/github.sdk 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SECURITY.md CHANGED
@@ -1,5 +1,5 @@
1
- # Reporting Security Issues
2
-
3
- To report a security issue, please [publish a private security advisory](https://github.com/OpenAlly/github.sdk/security/advisories) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
4
-
5
- Our vulnerability management team will respond within one week. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline.
1
+ # Reporting Security Issues
2
+
3
+ To report a security issue, please [publish a private security advisory](https://github.com/OpenAlly/github.sdk/security/advisories) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue.
4
+
5
+ Our vulnerability management team will respond within one week. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline.
@@ -10,12 +10,12 @@ const endpoint = repos.nodejs.node.pulls()
10
10
  .setAgent("my-app/1.0.0");
11
11
 
12
12
  // Stream items one by one across all pages
13
- for await (const pr of endpoint.iterate()) {
13
+ for await (const pr of endpoint) {
14
14
  console.log(pr.title);
15
15
  }
16
16
 
17
17
  // Or collect everything at once
18
- const allPRs = await endpoint.all();
18
+ const allPRs = await endpoint;
19
19
  ```
20
20
 
21
21
  ## Methods
@@ -35,3 +35,32 @@ Asynchronously iterates over all items across all pages. Pagination is handled t
35
35
  ### `.all(): Promise<T[]>`
36
36
 
37
37
  Collects all pages and resolves with a flat array of every item.
38
+
39
+ ### `[Symbol.asyncIterator](): AsyncIterableIterator<T>`
40
+
41
+ Makes `ApiEndpoint` compatible with `for await...of` loops. Internally delegates to `.iterate()`, so pagination is handled transparently.
42
+
43
+ ```ts
44
+ for await (const pr of endpoint) {
45
+ console.log(pr.title);
46
+ }
47
+ ```
48
+
49
+ ### `.then(onfulfilled?, onrejected?): Promise<TResult1 | TResult2>`
50
+
51
+ Makes `ApiEndpoint` a **Thenable**, so it can be `await`-ed directly without calling `.all()` explicitly. Internally delegates to `.all()`.
52
+
53
+ ```ts
54
+ // These two are equivalent:
55
+ const prs = await endpoint;
56
+ const prs = await endpoint.all();
57
+ ```
58
+
59
+ This also means `ApiEndpoint` instances can be passed to `Promise.resolve()`, used in `Promise.all()`, and chained with `.then()` / `.catch()` / `.finally()` like a regular promise.
60
+
61
+ ```ts
62
+ endpoint
63
+ .then((prs) => prs.filter((pr) => pr.draft === false))
64
+ .then(console.log)
65
+ .catch(console.error);
66
+ ```
@@ -11,15 +11,15 @@ const github = new GithubClient({
11
11
  });
12
12
 
13
13
  // Iterate over all open pull requests
14
- for await (const pr of github.repos.OpenAlly["github.sdk"].pulls().iterate()) {
14
+ for await (const pr of github.repos.OpenAlly["github.sdk"].pulls()) {
15
15
  console.log(pr.title);
16
16
  }
17
17
 
18
18
  // Collect all tags at once
19
- const tags = await github.repos.OpenAlly["github.sdk"].tags().all();
19
+ const tags = await github.repos.OpenAlly["github.sdk"].tags();
20
20
 
21
21
  // List all repositories for a user
22
- const userRepos = await github.users.torvalds.repos().all();
22
+ const userRepos = await github.users.torvalds.repos();
23
23
  ```
24
24
 
25
25
  ## Constructor
@@ -0,0 +1,106 @@
1
+ # fetchRawFile
2
+
3
+ Fetches the raw content of a file from a GitHub repository via `raw.githubusercontent.com`.
4
+
5
+ ```ts
6
+ import { fetchRawFile } from "@openally/github.sdk";
7
+
8
+ // Fetch file as plain text (default)
9
+ const content = await fetchRawFile("nodejs/node", "README.md");
10
+
11
+ // Fetch and parse as JSON
12
+ const pkg = await fetchRawFile<{ version: string }>("nodejs/node", "package.json", {
13
+ parser: "json"
14
+ });
15
+
16
+ // Fetch and parse with a custom parser
17
+ const lines = await fetchRawFile("nodejs/node", ".gitignore", {
18
+ parser: (content) => content.split("\n").filter(Boolean)
19
+ });
20
+
21
+ // Fetch from a specific branch or tag
22
+ const content = await fetchRawFile("nodejs/node", "README.md", {
23
+ ref: "v20.0.0"
24
+ });
25
+
26
+ // Fetch a private file with a token
27
+ const content = await fetchRawFile("myorg/private-repo", "config.json", {
28
+ token: process.env.GITHUB_TOKEN,
29
+ parser: "json"
30
+ });
31
+ ```
32
+
33
+ ## Signature
34
+
35
+ ```ts
36
+ function fetchRawFile(
37
+ repository: `${string}/${string}`,
38
+ filePath: string,
39
+ options?: FetchRawFileOptions
40
+ ): Promise<string>;
41
+
42
+ function fetchRawFile<T>(
43
+ repository: `${string}/${string}`,
44
+ filePath: string,
45
+ options: FetchRawFileOptions & { parser: "json" }
46
+ ): Promise<T>;
47
+
48
+ function fetchRawFile<T>(
49
+ repository: `${string}/${string}`,
50
+ filePath: string,
51
+ options: FetchRawFileOptions & { parser: (content: string) => T }
52
+ ): Promise<T>;
53
+ ```
54
+
55
+ ## Parameters
56
+
57
+ ### `repository`
58
+
59
+ Type: `` `${string}/${string}` ``
60
+
61
+ The repository in `owner/repo` format (e.g. `"nodejs/node"`).
62
+
63
+ ### `filePath`
64
+
65
+ Type: `string`
66
+
67
+ Path to the file within the repository (e.g. `"src/index.ts"` or `"README.md"`).
68
+
69
+ ### `options`
70
+
71
+ ```ts
72
+ interface FetchRawFileOptions extends RequestConfig {
73
+ /**
74
+ * Branch, tag, or commit SHA.
75
+ * @default "HEAD"
76
+ */
77
+ ref?: string;
78
+ }
79
+
80
+ interface RequestConfig {
81
+ /**
82
+ * A personal access token is required to access private resources,
83
+ * and to increase the rate limit for unauthenticated requests.
84
+ */
85
+ token?: string;
86
+ /**
87
+ * @default "@openally/github.sdk/1.0.0"
88
+ * @see https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#user-agent
89
+ */
90
+ userAgent?: string;
91
+ }
92
+
93
+ ```
94
+
95
+ ## Return value
96
+
97
+ - `Promise<string>` when no `parser` is provided.
98
+ - `Promise<T>` when `parser: "json"` or a custom parser function is provided.
99
+
100
+ ## Errors
101
+
102
+ Throws an `Error` if the HTTP response is not `ok` (e.g. 404 for a missing file, 401 for an unauthorized request):
103
+
104
+ ```
105
+ Failed to fetch raw file 'README.md' from nodejs/node@HEAD: HTTP 404
106
+ ```
package/docs/api/repos.md CHANGED
@@ -6,20 +6,20 @@ The `repos` proxy provides access to GitHub repository endpoints.
6
6
  import { repos } from "@openally/github.sdk";
7
7
 
8
8
  // Collect all tags
9
- const tags = await repos.OpenAlly["github.sdk"].tags().all();
9
+ const tags = await repos.OpenAlly["github.sdk"].tags();
10
10
 
11
11
  // Stream pull requests page by page
12
- for await (const pr of repos.nodejs.node.pulls().iterate()) {
12
+ for await (const pr of repos.nodejs.node.pulls()) {
13
13
  console.log(pr.number, pr.title);
14
14
  }
15
15
 
16
16
  // Stream workflow runs for a specific workflow file
17
- for await (const run of repos.nodejs.node.workflowRuns("ci.yml").iterate()) {
17
+ for await (const run of repos.nodejs.node.workflowRuns("ci.yml")) {
18
18
  console.log(run.id, run.status);
19
19
  }
20
20
 
21
21
  // Collect all jobs for a specific run
22
- const jobs = await repos.nodejs.node.runJobs(12345678).all();
22
+ const jobs = await repos.nodejs.node.runJobs(12345678);
23
23
  ```
24
24
 
25
25
  ## Access pattern
package/docs/api/users.md CHANGED
@@ -6,15 +6,15 @@ The `users` proxy provides access to GitHub user endpoints.
6
6
  import { users } from "@openally/github.sdk";
7
7
 
8
8
  // Collect all repositories for a user
9
- const repos = await users.torvalds.repos().all();
9
+ const repos = await users.torvalds.repos();
10
10
 
11
11
  // Stream followers one by one
12
- for await (const follower of users.torvalds.followers().iterate()) {
12
+ for await (const follower of users.torvalds.followers()) {
13
13
  console.log(follower.login);
14
14
  }
15
15
 
16
16
  // Collect all starred repositories
17
- const starred = await users.torvalds.starred().all();
17
+ const starred = await users.torvalds.starred();
18
18
  ```
19
19
 
20
20
  ## Access pattern
package/eslint.config.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { typescriptConfig } from "@openally/config.eslint";
2
-
3
- export default typescriptConfig();
1
+ import { typescriptConfig } from "@openally/config.eslint";
2
+
3
+ export default typescriptConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openally/github.sdk",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Opiniated Node.js Github SDK",
5
5
  "type": "module",
6
6
  "exports": "./dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "publishConfig": {
17
17
  "registry": "https://registry.npmjs.org",
18
18
  "access": "public",
19
- "provenance": false
19
+ "provenance": true
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
@@ -0,0 +1,73 @@
1
+ // Import Internal Dependencies
2
+ import {
3
+ DEFAULT_USER_AGENT,
4
+ GITHUB_RAW_API
5
+ } from "../constants.ts";
6
+ import type { RequestConfig } from "../types.ts";
7
+
8
+ // CONSTANTS
9
+ const kDefaultRef = "HEAD";
10
+
11
+ export interface FetchRawFileOptions extends RequestConfig {
12
+ /**
13
+ * Branch, tag, or commit SHA.
14
+ * @default "HEAD"
15
+ */
16
+ ref?: string;
17
+ }
18
+
19
+ export type FetchRawFileClientOptions = Omit<FetchRawFileOptions, "token" | "userAgent">;
20
+ export type RawFileParser<T> = "json" | ((content: string) => T);
21
+
22
+ export function fetchRawFile(
23
+ repository: `${string}/${string}`,
24
+ filePath: string,
25
+ options?: FetchRawFileOptions & { parser?: undefined; }
26
+ ): Promise<string>;
27
+ export function fetchRawFile<T = unknown>(
28
+ repository: `${string}/${string}`,
29
+ filePath: string,
30
+ options: FetchRawFileOptions & { parser: "json"; }
31
+ ): Promise<T>;
32
+ export function fetchRawFile<T>(
33
+ repository: `${string}/${string}`,
34
+ filePath: string,
35
+ options: FetchRawFileOptions & { parser: (content: string) => T; }
36
+ ): Promise<T>;
37
+ export async function fetchRawFile<T>(
38
+ repository: `${string}/${string}`,
39
+ filePath: string,
40
+ options: FetchRawFileOptions & { parser?: RawFileParser<T>; } = {}
41
+ ): Promise<string | T> {
42
+ const {
43
+ ref = kDefaultRef,
44
+ token,
45
+ userAgent = DEFAULT_USER_AGENT,
46
+ parser
47
+ } = options;
48
+
49
+ const url = new URL(`${repository}/${ref}/${filePath}`, GITHUB_RAW_API);
50
+ const headers: Record<string, string> = {
51
+ "User-Agent": userAgent,
52
+ ...(typeof token === "string" ? { Authorization: `token ${token}` } : {})
53
+ };
54
+
55
+ const response = await fetch(url, { headers });
56
+
57
+ if (!response.ok) {
58
+ throw new Error(
59
+ `Failed to fetch raw file '${filePath}' from ${repository}@${ref}: HTTP ${response.status}`
60
+ );
61
+ }
62
+
63
+ const content = await response.text();
64
+
65
+ if (parser === "json") {
66
+ return JSON.parse(content) as T;
67
+ }
68
+ if (typeof parser === "function") {
69
+ return parser(content);
70
+ }
71
+
72
+ return content;
73
+ }
package/src/api/repos.ts CHANGED
@@ -46,24 +46,38 @@ function createRepoProxy(
46
46
  commits: () => new ApiEndpoint<Commit>(`/repos/${owner}/${repo}/commits`, config),
47
47
  workflows: () => new ApiEndpoint<Workflow>(
48
48
  `/repos/${owner}/${repo}/actions/workflows`,
49
- { ...config, extractor: (raw: WorkflowsResponse) => raw.workflows }
49
+ {
50
+ ...config,
51
+ extractor: (raw: WorkflowsResponse) => raw.workflows
52
+ }
50
53
  ),
51
54
  workflowRuns: (workflowId: string | number) => new ApiEndpoint<WorkflowRun>(
52
55
  `/repos/${owner}/${repo}/actions/workflows/${workflowId}/runs`,
53
- { ...config, extractor: (raw: WorkflowRunsResponse) => raw.workflow_runs }
56
+ {
57
+ ...config,
58
+ extractor: (raw: WorkflowRunsResponse) => raw.workflow_runs
59
+ }
54
60
  ),
55
61
  runJobs: (runId: number) => new ApiEndpoint<Job>(
56
62
  `/repos/${owner}/${repo}/actions/runs/${runId}/jobs`,
57
- { ...config, extractor: (raw: JobsResponse) => raw.jobs }
63
+ {
64
+ ...config,
65
+ extractor: (raw: JobsResponse) => raw.jobs
66
+ }
58
67
  ),
59
68
  runArtifacts: (runId: number) => new ApiEndpoint<Artifact>(
60
69
  `/repos/${owner}/${repo}/actions/runs/${runId}/artifacts`,
61
- { ...config, extractor: (raw: ArtifactsResponse) => raw.artifacts }
70
+ {
71
+ ...config,
72
+ extractor: (raw: ArtifactsResponse) => raw.artifacts
73
+ }
62
74
  )
63
75
  };
64
76
  }
65
77
 
66
- export function createReposProxy(config: RequestConfig = {}): ReposProxy {
78
+ export function createReposProxy(
79
+ config: RequestConfig = {}
80
+ ): ReposProxy {
67
81
  return createApiProxy(
68
82
  (owner) => createApiProxy(
69
83
  (repo) => createRepoProxy(owner, repo, config)
package/src/api/users.ts CHANGED
@@ -32,16 +32,20 @@ export type UsersProxy = {
32
32
  function createUserProxy(
33
33
  username: string,
34
34
  config: RequestConfig = {}
35
- ): UserEndpointMethods {
35
+ ) {
36
36
  return Object.fromEntries(
37
- (Object.keys(kUserEndpointResponseMap) as UserEndpoint[]).map(
37
+ Object.keys(kUserEndpointResponseMap).map(
38
38
  (endpoint) => [endpoint, () => new ApiEndpoint(`/users/${username}/${endpoint}`, config)]
39
39
  )
40
- ) as UserEndpointMethods;
40
+ );
41
41
  }
42
42
 
43
- export function createUsersProxy(config: RequestConfig = {}): UsersProxy {
44
- return createApiProxy((username) => createUserProxy(username, config)) as UsersProxy;
43
+ export function createUsersProxy(
44
+ config: RequestConfig = {}
45
+ ): UsersProxy {
46
+ return createApiProxy(
47
+ (username) => createUserProxy(username, config)
48
+ ) as UsersProxy;
45
49
  }
46
50
 
47
51
  export const users = createUsersProxy();
@@ -1,26 +1,18 @@
1
1
  // Import Internal Dependencies
2
2
  import { HttpLinkParser } from "./HttpLinkParser.ts";
3
+ import {
4
+ DEFAULT_USER_AGENT,
5
+ GITHUB_API
6
+ } from "../constants.ts";
7
+ import type { RequestConfig } from "../types.ts";
3
8
 
4
- // CONSTANTS
5
- const kGithubURL = new URL("https://api.github.com/");
6
-
7
- export class ApiEndpointOptions<T> {
9
+ export interface ApiEndpointOptions<T> extends RequestConfig {
8
10
  /**
9
11
  * By default, the raw response from the GitHub API is returned as-is.
10
12
  * You can provide a custom extractor function to transform the raw response
11
13
  * into an array of type T.
12
14
  */
13
15
  extractor?: (raw: any) => T[];
14
- /**
15
- * A personal access token is required to access private resources,
16
- * and to increase the rate limit for unauthenticated requests.
17
- */
18
- token?: string;
19
- /**
20
- * @default "@openally/github.sdk/1.0.0"
21
- * @see https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#user-agent
22
- */
23
- userAgent?: string;
24
16
  }
25
17
 
26
18
  export class ApiEndpoint<T> {
@@ -36,7 +28,7 @@ export class ApiEndpoint<T> {
36
28
  options: ApiEndpointOptions<T> = {}
37
29
  ) {
38
30
  const {
39
- userAgent = "@openally/github.sdk/1.0.0",
31
+ userAgent = DEFAULT_USER_AGENT,
40
32
  token,
41
33
  extractor = ((raw) => raw as T[])
42
34
  } = options;
@@ -75,8 +67,8 @@ export class ApiEndpoint<T> {
75
67
  };
76
68
 
77
69
  const url = this.#nextURL === null ?
78
- new URL(this.#apiEndpoint, kGithubURL) :
79
- new URL(this.#nextURL, kGithubURL);
70
+ new URL(this.#apiEndpoint, GITHUB_API) :
71
+ new URL(this.#nextURL, GITHUB_API);
80
72
  const response = await fetch(
81
73
  url,
82
74
  { headers }
@@ -99,7 +91,18 @@ export class ApiEndpoint<T> {
99
91
  } while (this.#nextURL !== null);
100
92
  }
101
93
 
94
+ [Symbol.asyncIterator](): AsyncIterableIterator<T> {
95
+ return this.iterate();
96
+ }
97
+
102
98
  all(): Promise<T[]> {
103
99
  return Array.fromAsync(this.iterate());
104
100
  }
101
+
102
+ then<TResult1 = T[], TResult2 = never>(
103
+ onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null,
104
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
105
+ ): Promise<TResult1 | TResult2> {
106
+ return this.all().then(onfulfilled, onrejected);
107
+ }
105
108
  }
@@ -7,25 +7,56 @@ import {
7
7
  createReposProxy,
8
8
  type ReposProxy
9
9
  } from "../api/repos.ts";
10
+ import {
11
+ fetchRawFile,
12
+ type FetchRawFileClientOptions,
13
+ type RawFileParser
14
+ } from "../api/rawFile.ts";
15
+ import type { RequestConfig } from "../types.ts";
10
16
 
11
- export interface GithubClientOptions {
12
- token?: string;
13
- userAgent?: string;
14
- }
17
+ export interface GithubClientOptions extends RequestConfig {}
15
18
 
16
19
  export class GithubClient {
17
20
  readonly users: UsersProxy;
18
21
  readonly repos: ReposProxy;
22
+ #config: RequestConfig;
19
23
 
20
24
  constructor(
21
25
  options: GithubClientOptions = {}
22
26
  ) {
23
- const config = {
27
+ this.#config = {
24
28
  token: options.token,
25
29
  userAgent: options.userAgent
26
30
  };
27
31
 
28
- this.users = createUsersProxy(config);
29
- this.repos = createReposProxy(config);
32
+ this.users = createUsersProxy(this.#config);
33
+ this.repos = createReposProxy(this.#config);
34
+ }
35
+
36
+ fetchRawFile(
37
+ repository: `${string}/${string}`,
38
+ filePath: string,
39
+ options?: FetchRawFileClientOptions & { parser?: undefined; }
40
+ ): Promise<string>;
41
+ fetchRawFile<T = unknown>(
42
+ repository: `${string}/${string}`,
43
+ filePath: string,
44
+ options: FetchRawFileClientOptions & { parser: "json"; }
45
+ ): Promise<T>;
46
+ fetchRawFile<T>(
47
+ repository: `${string}/${string}`,
48
+ filePath: string,
49
+ options: FetchRawFileClientOptions & { parser: (content: string) => T; }
50
+ ): Promise<T>;
51
+ fetchRawFile<T>(
52
+ repository: `${string}/${string}`,
53
+ filePath: string,
54
+ options: FetchRawFileClientOptions & { parser?: RawFileParser<T>; } = {}
55
+ ): Promise<string | T> {
56
+ return fetchRawFile<T>(
57
+ repository,
58
+ filePath,
59
+ { ...this.#config, ...options } as any
60
+ );
30
61
  }
31
62
  }
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_USER_AGENT = "@openally/github.sdk/1.0.0";
2
+ export const GITHUB_API = new URL("https://api.github.com/");
3
+ export const GITHUB_RAW_API = new URL("https://raw.githubusercontent.com/");
package/src/index.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export * from "./api/users.ts";
2
2
  export * from "./api/repos.ts";
3
+ export {
4
+ fetchRawFile,
5
+ type FetchRawFileOptions
6
+ } from "./api/rawFile.ts";
3
7
  export * from "./class/GithubClient.ts";
4
8
  export type { RequestConfig } from "./types.ts";
5
9
 
package/src/types.ts CHANGED
@@ -2,7 +2,15 @@
2
2
  import type { Endpoints } from "@octokit/types";
3
3
 
4
4
  export interface RequestConfig {
5
+ /**
6
+ * A personal access token is required to access private resources,
7
+ * and to increase the rate limit for unauthenticated requests.
8
+ */
5
9
  token?: string;
10
+ /**
11
+ * @default "@openally/github.sdk/1.0.0"
12
+ * @see https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#user-agent
13
+ */
6
14
  userAgent?: string;
7
15
  }
8
16
 
@@ -140,6 +140,21 @@ describe("ApiEndpoint", () => {
140
140
  });
141
141
  });
142
142
 
143
+ describe("all() with thenable", () => {
144
+ it("should fetch a single page and return all items", async() => {
145
+ mockAgent
146
+ .get(kGithubOrigin)
147
+ .intercept({ path: "/users/foo/repos", method: "GET" })
148
+ .reply(200, JSON.stringify([{ id: 1 }, { id: 2 }]), {
149
+ headers: { "content-type": "application/json" }
150
+ });
151
+
152
+ const result = await new ApiEndpoint("/users/foo/repos");
153
+
154
+ assert.deepEqual(result, [{ id: 1 }, { id: 2 }]);
155
+ });
156
+ });
157
+
143
158
  describe("iterate()", () => {
144
159
  it("should yield items one at a time", async() => {
145
160
  mockAgent
@@ -184,6 +199,50 @@ describe("ApiEndpoint", () => {
184
199
  });
185
200
  });
186
201
 
202
+ describe("Symbol.asyncIterator", () => {
203
+ it("should yield items one at a time", async() => {
204
+ mockAgent
205
+ .get(kGithubOrigin)
206
+ .intercept({ path: "/users/foo/repos", method: "GET" })
207
+ .reply(200, JSON.stringify([{ id: 1 }, { id: 2 }]), {
208
+ headers: { "content-type": "application/json" }
209
+ });
210
+
211
+ const items: unknown[] = [];
212
+ for await (const item of new ApiEndpoint("/users/foo/repos")) {
213
+ items.push(item);
214
+ }
215
+
216
+ assert.deepEqual(items, [{ id: 1 }, { id: 2 }]);
217
+ });
218
+
219
+ it("should yield items across paginated pages", async() => {
220
+ const pool = mockAgent.get(kGithubOrigin);
221
+
222
+ pool
223
+ .intercept({ path: "/users/foo/repos", method: "GET" })
224
+ .reply(200, JSON.stringify([{ id: 1 }]), {
225
+ headers: {
226
+ "content-type": "application/json",
227
+ link: '<https://api.github.com/users/foo/repos?page=2>; rel="next"'
228
+ }
229
+ });
230
+
231
+ pool
232
+ .intercept({ path: "/users/foo/repos?page=2", method: "GET" })
233
+ .reply(200, JSON.stringify([{ id: 2 }, { id: 3 }]), {
234
+ headers: { "content-type": "application/json" }
235
+ });
236
+
237
+ const items: unknown[] = [];
238
+ for await (const item of new ApiEndpoint("/users/foo/repos")) {
239
+ items.push(item);
240
+ }
241
+
242
+ assert.deepEqual(items, [{ id: 1 }, { id: 2 }, { id: 3 }]);
243
+ });
244
+ });
245
+
187
246
  describe("headers", () => {
188
247
  it("should send the default User-Agent header", async() => {
189
248
  mockAgent
@@ -257,10 +316,12 @@ describe("ApiEndpoint", () => {
257
316
 
258
317
  describe("extractor", () => {
259
318
  it("should apply the extractor to transform the raw response", async() => {
319
+ const workflows = [{ id: 10 }, { id: 20 }];
320
+
260
321
  mockAgent
261
322
  .get(kGithubOrigin)
262
323
  .intercept({ path: "/repos/foo/bar/actions/workflows", method: "GET" })
263
- .reply(200, JSON.stringify({ total_count: 2, workflows: [{ id: 10 }, { id: 20 }] }), {
324
+ .reply(200, JSON.stringify({ total_count: 2, workflows }), {
264
325
  headers: { "content-type": "application/json" }
265
326
  });
266
327
 
@@ -269,7 +330,7 @@ describe("ApiEndpoint", () => {
269
330
  { extractor: (raw) => raw.workflows }
270
331
  ).all();
271
332
 
272
- assert.deepEqual(result, [{ id: 10 }, { id: 20 }]);
333
+ assert.deepEqual(result, workflows);
273
334
  });
274
335
 
275
336
  it("should apply the extractor on every page when paginating", async() => {