Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ const userRepos = await github.users.torvalds.repos();
- [ApiEndpoint](./docs/api/ApiEndpoint.md)
- [GithubClient](./docs/api/GithubClient.md)

Available GitHub APIs:
Available GitHub APIs:

- [repos](./docs/api/repos.md)
- [users](./docs/api/users.md)
- [fetchRawFile](./docs/api/fetchRawFile.md)

---

Expand Down
106 changes: 106 additions & 0 deletions docs/api/fetchRawFile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# fetchRawFile

Fetches the raw content of a file from a GitHub repository via `raw.githubusercontent.com`.

```ts
import { fetchRawFile } from "@openally/github.sdk";

// Fetch file as plain text (default)
const content = await fetchRawFile("nodejs/node", "README.md");

// Fetch and parse as JSON
const pkg = await fetchRawFile<{ version: string }>("nodejs/node", "package.json", {
parser: "json"
});

// Fetch and parse with a custom parser
const lines = await fetchRawFile("nodejs/node", ".gitignore", {
parser: (content) => content.split("\n").filter(Boolean)
});

// Fetch from a specific branch or tag
const content = await fetchRawFile("nodejs/node", "README.md", {
ref: "v20.0.0"
});

// Fetch a private file with a token
const content = await fetchRawFile("myorg/private-repo", "config.json", {
token: process.env.GITHUB_TOKEN,
parser: "json"
});
```

## Signature

```ts
function fetchRawFile(
repository: `${string}/${string}`,
filePath: string,
options?: FetchRawFileOptions
): Promise<string>;

function fetchRawFile<T>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileOptions & { parser: "json" }
): Promise<T>;

function fetchRawFile<T>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileOptions & { parser: (content: string) => T }
): Promise<T>;
```

## Parameters

### `repository`

Type: `` `${string}/${string}` ``

The repository in `owner/repo` format (e.g. `"nodejs/node"`).

### `filePath`

Type: `string`

Path to the file within the repository (e.g. `"src/index.ts"` or `"README.md"`).

### `options`

```ts
interface FetchRawFileOptions extends RequestConfig {
/**
* Branch, tag, or commit SHA.
* @default "HEAD"
*/
ref?: string;
}

interface RequestConfig {
/**
* A personal access token is required to access private resources,
* and to increase the rate limit for unauthenticated requests.
*/
token?: string;
/**
* @default "@openally/github.sdk/1.0.0"
* @see https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#user-agent
*/
userAgent?: string;
}

```

## Return value

- `Promise<string>` when no `parser` is provided.
- `Promise<T>` when `parser: "json"` or a custom parser function is provided.

## Errors

Throws an `Error` if the HTTP response is not `ok` (e.g. 404 for a missing file, 401 for an unauthorized request):

```
Failed to fetch raw file 'README.md' from nodejs/node@HEAD: HTTP 404
```
73 changes: 73 additions & 0 deletions src/api/rawFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Import Internal Dependencies
import {
DEFAULT_USER_AGENT,
GITHUB_RAW_API
} from "../constants.ts";
import type { RequestConfig } from "../types.ts";

// CONSTANTS
const kDefaultRef = "HEAD";

export interface FetchRawFileOptions extends RequestConfig {
/**
* Branch, tag, or commit SHA.
* @default "HEAD"
*/
ref?: string;
}

export type FetchRawFileClientOptions = Omit<FetchRawFileOptions, "token" | "userAgent">;
export type RawFileParser<T> = "json" | ((content: string) => T);

export function fetchRawFile(
repository: `${string}/${string}`,
filePath: string,
options?: FetchRawFileOptions & { parser?: undefined; }
): Promise<string>;
export function fetchRawFile<T = unknown>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileOptions & { parser: "json"; }
): Promise<T>;
export function fetchRawFile<T>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileOptions & { parser: (content: string) => T; }
): Promise<T>;
export async function fetchRawFile<T>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileOptions & { parser?: RawFileParser<T>; } = {}
): Promise<string | T> {
const {
ref = kDefaultRef,
token,
userAgent = DEFAULT_USER_AGENT,
parser
} = options;

const url = new URL(`${repository}/${ref}/${filePath}`, GITHUB_RAW_API);
const headers: Record<string, string> = {
"User-Agent": userAgent,
...(typeof token === "string" ? { Authorization: `token ${token}` } : {})
};

const response = await fetch(url, { headers });

if (!response.ok) {
throw new Error(
`Failed to fetch raw file '${filePath}' from ${repository}@${ref}: HTTP ${response.status}`
);
}

const content = await response.text();

if (parser === "json") {
return JSON.parse(content) as T;
}
if (typeof parser === "function") {
return parser(content);
}

return content;
}
26 changes: 9 additions & 17 deletions src/class/ApiEndpoint.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
// Import Internal Dependencies
import { HttpLinkParser } from "./HttpLinkParser.ts";
import {
DEFAULT_USER_AGENT,
GITHUB_API
} from "../constants.ts";
import type { RequestConfig } from "../types.ts";

// CONSTANTS
const kGithubURL = new URL("https://api.github.com/");

export class ApiEndpointOptions<T> {
export interface ApiEndpointOptions<T> extends RequestConfig {
/**
* By default, the raw response from the GitHub API is returned as-is.
* You can provide a custom extractor function to transform the raw response
* into an array of type T.
*/
extractor?: (raw: any) => T[];
/**
* A personal access token is required to access private resources,
* and to increase the rate limit for unauthenticated requests.
*/
token?: string;
/**
* @default "@openally/github.sdk/1.0.0"
* @see https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#user-agent
*/
userAgent?: string;
}

export class ApiEndpoint<T> {
Expand All @@ -36,7 +28,7 @@ export class ApiEndpoint<T> {
options: ApiEndpointOptions<T> = {}
) {
const {
userAgent = "@openally/github.sdk/1.0.0",
userAgent = DEFAULT_USER_AGENT,
token,
extractor = ((raw) => raw as T[])
} = options;
Expand Down Expand Up @@ -75,8 +67,8 @@ export class ApiEndpoint<T> {
};

const url = this.#nextURL === null ?
new URL(this.#apiEndpoint, kGithubURL) :
new URL(this.#nextURL, kGithubURL);
new URL(this.#apiEndpoint, GITHUB_API) :
new URL(this.#nextURL, GITHUB_API);
const response = await fetch(
url,
{ headers }
Expand Down
45 changes: 38 additions & 7 deletions src/class/GithubClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,56 @@ import {
createReposProxy,
type ReposProxy
} from "../api/repos.ts";
import {
fetchRawFile,
type FetchRawFileClientOptions,
type RawFileParser
} from "../api/rawFile.ts";
import type { RequestConfig } from "../types.ts";

export interface GithubClientOptions {
token?: string;
userAgent?: string;
}
export interface GithubClientOptions extends RequestConfig {}

export class GithubClient {
readonly users: UsersProxy;
readonly repos: ReposProxy;
#config: RequestConfig;

constructor(
options: GithubClientOptions = {}
) {
const config = {
this.#config = {
token: options.token,
userAgent: options.userAgent
};

this.users = createUsersProxy(config);
this.repos = createReposProxy(config);
this.users = createUsersProxy(this.#config);
this.repos = createReposProxy(this.#config);
}

fetchRawFile(
repository: `${string}/${string}`,
filePath: string,
options?: FetchRawFileClientOptions & { parser?: undefined; }
): Promise<string>;
fetchRawFile<T = unknown>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileClientOptions & { parser: "json"; }
): Promise<T>;
fetchRawFile<T>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileClientOptions & { parser: (content: string) => T; }
): Promise<T>;
fetchRawFile<T>(
repository: `${string}/${string}`,
filePath: string,
options: FetchRawFileClientOptions & { parser?: RawFileParser<T>; } = {}
): Promise<string | T> {
return fetchRawFile<T>(
repository,
filePath,
{ ...this.#config, ...options } as any
);
}
}
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DEFAULT_USER_AGENT = "@openally/github.sdk/1.0.0";
export const GITHUB_API = new URL("https://api.github.com/");
export const GITHUB_RAW_API = new URL("https://raw.githubusercontent.com/");
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export * from "./api/users.ts";
export * from "./api/repos.ts";
export {
fetchRawFile,
type FetchRawFileOptions
} from "./api/rawFile.ts";
export * from "./class/GithubClient.ts";
export type { RequestConfig } from "./types.ts";

8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
import type { Endpoints } from "@octokit/types";

export interface RequestConfig {
/**
* A personal access token is required to access private resources,
* and to increase the rate limit for unauthenticated requests.
*/
token?: string;
/**
* @default "@openally/github.sdk/1.0.0"
* @see https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#user-agent
*/
userAgent?: string;
}

Expand Down
Loading