@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/.all-contributorsrc +16 -16
- package/.editorconfig +13 -13
- package/.github/dependabot.yml +25 -25
- package/.github/workflows/codeql.yml +78 -78
- package/.github/workflows/node.js.yml +52 -52
- package/.github/workflows/publish.yml +29 -29
- package/.github/workflows/scorecards.yml +76 -76
- package/LICENSE +21 -21
- package/README.md +5 -4
- package/SECURITY.md +5 -5
- package/docs/api/ApiEndpoint.md +31 -2
- package/docs/api/GithubClient.md +3 -3
- package/docs/api/fetchRawFile.md +106 -0
- package/docs/api/repos.md +4 -4
- package/docs/api/users.md +3 -3
- package/eslint.config.mjs +3 -3
- package/package.json +2 -2
- package/src/api/rawFile.ts +73 -0
- package/src/api/repos.ts +19 -5
- package/src/api/users.ts +9 -5
- package/src/class/ApiEndpoint.ts +20 -17
- package/src/class/GithubClient.ts +38 -7
- package/src/constants.ts +3 -0
- package/src/index.ts +4 -0
- package/src/types.ts +8 -0
- package/test/ApiEndpoint.spec.ts +63 -2
- package/test/GithubClient.spec.ts +28 -9
- package/test/createApiProxy.spec.ts +4 -4
- package/test/rawFile.spec.ts +382 -0
- package/test/tsconfig.json +11 -0
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.
|
package/docs/api/ApiEndpoint.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
```
|
package/docs/api/GithubClient.md
CHANGED
|
@@ -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()
|
|
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()
|
|
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()
|
|
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()
|
|
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()
|
|
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")
|
|
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)
|
|
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()
|
|
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()
|
|
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()
|
|
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.
|
|
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":
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
70
|
+
{
|
|
71
|
+
...config,
|
|
72
|
+
extractor: (raw: ArtifactsResponse) => raw.artifacts
|
|
73
|
+
}
|
|
62
74
|
)
|
|
63
75
|
};
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
export function createReposProxy(
|
|
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
|
-
)
|
|
35
|
+
) {
|
|
36
36
|
return Object.fromEntries(
|
|
37
|
-
|
|
37
|
+
Object.keys(kUserEndpointResponseMap).map(
|
|
38
38
|
(endpoint) => [endpoint, () => new ApiEndpoint(`/users/${username}/${endpoint}`, config)]
|
|
39
39
|
)
|
|
40
|
-
)
|
|
40
|
+
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function createUsersProxy(
|
|
44
|
-
|
|
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();
|
package/src/class/ApiEndpoint.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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,
|
|
79
|
-
new URL(this.#nextURL,
|
|
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
|
-
|
|
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
|
}
|
package/src/constants.ts
ADDED
package/src/index.ts
CHANGED
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
|
|
package/test/ApiEndpoint.spec.ts
CHANGED
|
@@ -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
|
|
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,
|
|
333
|
+
assert.deepEqual(result, workflows);
|
|
273
334
|
});
|
|
274
335
|
|
|
275
336
|
it("should apply the extractor on every page when paginating", async() => {
|