Skip to content

Commit aa8ef08

Browse files
authored
feat: implement fetching raw file on github (#11)
1 parent b1c2a0e commit aa8ef08

9 files changed

Lines changed: 625 additions & 25 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ const userRepos = await github.users.torvalds.repos();
6161
- [ApiEndpoint](./docs/api/ApiEndpoint.md)
6262
- [GithubClient](./docs/api/GithubClient.md)
6363

64-
Available GitHub APIs:
64+
Available GitHub APIs:
6565

6666
- [repos](./docs/api/repos.md)
6767
- [users](./docs/api/users.md)
68+
- [fetchRawFile](./docs/api/fetchRawFile.md)
6869

6970
---
7071

docs/api/fetchRawFile.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
```

src/api/rawFile.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

src/class/ApiEndpoint.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
// Import Internal Dependencies
22
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";
38

4-
// CONSTANTS
5-
const kGithubURL = new URL("https://api.github.com/");
6-
7-
export class ApiEndpointOptions<T> {
9+
export interface ApiEndpointOptions<T> extends RequestConfig {
810
/**
911
* By default, the raw response from the GitHub API is returned as-is.
1012
* You can provide a custom extractor function to transform the raw response
1113
* into an array of type T.
1214
*/
1315
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;
2416
}
2517

2618
export class ApiEndpoint<T> {
@@ -36,7 +28,7 @@ export class ApiEndpoint<T> {
3628
options: ApiEndpointOptions<T> = {}
3729
) {
3830
const {
39-
userAgent = "@openally/github.sdk/1.0.0",
31+
userAgent = DEFAULT_USER_AGENT,
4032
token,
4133
extractor = ((raw) => raw as T[])
4234
} = options;
@@ -75,8 +67,8 @@ export class ApiEndpoint<T> {
7567
};
7668

7769
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);
8072
const response = await fetch(
8173
url,
8274
{ headers }

src/class/GithubClient.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,56 @@ import {
77
createReposProxy,
88
type ReposProxy
99
} 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";
1016

11-
export interface GithubClientOptions {
12-
token?: string;
13-
userAgent?: string;
14-
}
17+
export interface GithubClientOptions extends RequestConfig {}
1518

1619
export class GithubClient {
1720
readonly users: UsersProxy;
1821
readonly repos: ReposProxy;
22+
#config: RequestConfig;
1923

2024
constructor(
2125
options: GithubClientOptions = {}
2226
) {
23-
const config = {
27+
this.#config = {
2428
token: options.token,
2529
userAgent: options.userAgent
2630
};
2731

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+
);
3061
}
3162
}

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/");

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export * from "./api/users.ts";
22
export * from "./api/repos.ts";
3+
export {
4+
fetchRawFile,
5+
type FetchRawFileOptions
6+
} from "./api/rawFile.ts";
37
export * from "./class/GithubClient.ts";
48
export type { RequestConfig } from "./types.ts";
59

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
import type { Endpoints } from "@octokit/types";
33

44
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+
*/
59
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+
*/
614
userAgent?: string;
715
}
816

0 commit comments

Comments
 (0)