@openally/github.sdk 1.0.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.
@@ -0,0 +1,76 @@
1
+ # users
2
+
3
+ The `users` proxy provides access to GitHub user endpoints.
4
+
5
+ ```ts
6
+ import { users } from "@openally/github.sdk";
7
+
8
+ // Collect all repositories for a user
9
+ const repos = await users.torvalds.repos().all();
10
+
11
+ // Stream followers one by one
12
+ for await (const follower of users.torvalds.followers().iterate()) {
13
+ console.log(follower.login);
14
+ }
15
+
16
+ // Collect all starred repositories
17
+ const starred = await users.torvalds.starred().all();
18
+ ```
19
+
20
+ ## Access pattern
21
+
22
+ ```ts
23
+ users[username].<method>()
24
+ ```
25
+
26
+ All methods return an [`ApiEndpoint<T>`](./ApiEndpoint.md) instance.
27
+
28
+ ## Methods
29
+
30
+ ### `.orgs()`
31
+
32
+ Returns `ApiEndpoint<UserOrg>`.
33
+
34
+ Lists all organizations the user belongs to.
35
+
36
+ > GitHub docs: [List organizations for a user](https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-a-user)
37
+
38
+ ### `.repos()`
39
+
40
+ Returns `ApiEndpoint<UserRepo>`.
41
+
42
+ Lists all public repositories for the user.
43
+
44
+ > GitHub docs: [List repositories for a user](https://docs.github.com/en/rest/repos/repos#list-repositories-for-a-user)
45
+
46
+ ### `.gists()`
47
+
48
+ Returns `ApiEndpoint<UserGist>`.
49
+
50
+ Lists all public gists for the user.
51
+
52
+ > GitHub docs: [List gists for a user](https://docs.github.com/en/rest/gists/gists#list-gists-for-a-user)
53
+
54
+ ### `.followers()`
55
+
56
+ Returns `ApiEndpoint<UserFollower>`.
57
+
58
+ Lists all followers of the user.
59
+
60
+ > GitHub docs: [List followers of a user](https://docs.github.com/en/rest/users/followers#list-followers-of-a-user)
61
+
62
+ ### `.following()`
63
+
64
+ Returns `ApiEndpoint<UserFollowing>`.
65
+
66
+ Lists all users the user follows.
67
+
68
+ > GitHub docs: [List the people a user follows](https://docs.github.com/en/rest/users/followers#list-the-people-a-user-follows)
69
+
70
+ ### `.starred()`
71
+
72
+ Returns `ApiEndpoint<UserStarred>`.
73
+
74
+ Lists all repositories starred by the user.
75
+
76
+ > GitHub docs: [List repositories starred by a user](https://docs.github.com/en/rest/activity/starring#list-repositories-starred-by-a-user)
@@ -0,0 +1,3 @@
1
+ import { typescriptConfig } from "@openally/config.eslint";
2
+
3
+ export default typescriptConfig();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@openally/github.sdk",
3
+ "version": "1.0.0",
4
+ "description": "Opiniated Node.js Github SDK",
5
+ "type": "module",
6
+ "exports": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "prepublishOnly": "npm run build",
11
+ "test": "node --test ./test/**/*.spec.ts",
12
+ "coverage": "c8 --reporter=lcov npm run test",
13
+ "lint": "eslint .",
14
+ "lint:fix": "eslint . --fix"
15
+ },
16
+ "publishConfig": {
17
+ "registry": "https://registry.npmjs.org",
18
+ "access": "public",
19
+ "provenance": false
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/OpenAlly/github.sdk.git"
24
+ },
25
+ "keywords": [
26
+ "github",
27
+ "sdk",
28
+ "api",
29
+ "client",
30
+ "nodejs",
31
+ "typescript"
32
+ ],
33
+ "author": "GENTILHOMME Thomas <gentilhomme.thomas@gmail.com>",
34
+ "license": "MIT",
35
+ "bugs": {
36
+ "url": "https://github.com/OpenAlly/github.sdk/issues"
37
+ },
38
+ "homepage": "https://github.com/OpenAlly/github.sdk#readme",
39
+ "devDependencies": {
40
+ "@openally/config.eslint": "^2.2.0",
41
+ "@openally/config.typescript": "^1.2.1",
42
+ "@types/node": "25.3.1",
43
+ "c8": "^11.0.0",
44
+ "typescript": "^5.9.3",
45
+ "undici": "7.22.0"
46
+ },
47
+ "dependencies": {
48
+ "@octokit/types": "16.0.0"
49
+ }
50
+ }
@@ -0,0 +1,74 @@
1
+ // Import Internal Dependencies
2
+ import { ApiEndpoint } from "../class/ApiEndpoint.ts";
3
+ import { createApiProxy } from "../class/createApiProxy.ts";
4
+ import type {
5
+ Tag,
6
+ PullRequest,
7
+ Issue,
8
+ Commit,
9
+ Workflow,
10
+ WorkflowRun,
11
+ Job,
12
+ Artifact,
13
+ WorkflowsResponse,
14
+ WorkflowRunsResponse,
15
+ JobsResponse,
16
+ ArtifactsResponse,
17
+ RequestConfig
18
+ } from "../types.ts";
19
+
20
+ type RepoEndpointMethods = {
21
+ tags: () => ApiEndpoint<Tag>;
22
+ pulls: () => ApiEndpoint<PullRequest>;
23
+ issues: () => ApiEndpoint<Issue>;
24
+ commits: () => ApiEndpoint<Commit>;
25
+ workflows: () => ApiEndpoint<Workflow>;
26
+ workflowRuns: (workflowId: string | number) => ApiEndpoint<WorkflowRun>;
27
+ runJobs: (runId: number) => ApiEndpoint<Job>;
28
+ runArtifacts: (runId: number) => ApiEndpoint<Artifact>;
29
+ };
30
+
31
+ export type ReposProxy = {
32
+ [owner: string]: {
33
+ [repo: string]: RepoEndpointMethods;
34
+ };
35
+ };
36
+
37
+ function createRepoProxy(
38
+ owner: string,
39
+ repo: string,
40
+ config: RequestConfig = {}
41
+ ): RepoEndpointMethods {
42
+ return {
43
+ tags: () => new ApiEndpoint<Tag>(`/repos/${owner}/${repo}/tags`, config),
44
+ pulls: () => new ApiEndpoint<PullRequest>(`/repos/${owner}/${repo}/pulls`, config),
45
+ issues: () => new ApiEndpoint<Issue>(`/repos/${owner}/${repo}/issues`, config),
46
+ commits: () => new ApiEndpoint<Commit>(`/repos/${owner}/${repo}/commits`, config),
47
+ workflows: () => new ApiEndpoint<Workflow>(
48
+ `/repos/${owner}/${repo}/actions/workflows`,
49
+ { ...config, extractor: (raw: WorkflowsResponse) => raw.workflows }
50
+ ),
51
+ workflowRuns: (workflowId: string | number) => new ApiEndpoint<WorkflowRun>(
52
+ `/repos/${owner}/${repo}/actions/workflows/${workflowId}/runs`,
53
+ { ...config, extractor: (raw: WorkflowRunsResponse) => raw.workflow_runs }
54
+ ),
55
+ runJobs: (runId: number) => new ApiEndpoint<Job>(
56
+ `/repos/${owner}/${repo}/actions/runs/${runId}/jobs`,
57
+ { ...config, extractor: (raw: JobsResponse) => raw.jobs }
58
+ ),
59
+ runArtifacts: (runId: number) => new ApiEndpoint<Artifact>(
60
+ `/repos/${owner}/${repo}/actions/runs/${runId}/artifacts`,
61
+ { ...config, extractor: (raw: ArtifactsResponse) => raw.artifacts }
62
+ )
63
+ };
64
+ }
65
+
66
+ export function createReposProxy(config: RequestConfig = {}): ReposProxy {
67
+ return createApiProxy(
68
+ (owner) => createApiProxy(
69
+ (repo) => createRepoProxy(owner, repo, config)
70
+ )
71
+ ) as ReposProxy;
72
+ }
73
+
74
+ export const repos = createReposProxy();
@@ -0,0 +1,47 @@
1
+ // Import Internal Dependencies
2
+ import { ApiEndpoint } from "../class/ApiEndpoint.ts";
3
+ import { createApiProxy } from "../class/createApiProxy.ts";
4
+ import type {
5
+ UserOrg,
6
+ UserRepo,
7
+ UserGist,
8
+ UserFollower,
9
+ UserFollowing,
10
+ UserStarred,
11
+ RequestConfig
12
+ } from "../types.ts";
13
+
14
+ // CONSTANTS
15
+ const kUserEndpointResponseMap = {
16
+ orgs: {} as UserOrg,
17
+ repos: {} as UserRepo,
18
+ gists: {} as UserGist,
19
+ followers: {} as UserFollower,
20
+ following: {} as UserFollowing,
21
+ starred: {} as UserStarred
22
+ };
23
+
24
+ type UserEndpoint = keyof typeof kUserEndpointResponseMap;
25
+ type UserEndpointMethods = {
26
+ [K in UserEndpoint]: () => ApiEndpoint<typeof kUserEndpointResponseMap[K]>;
27
+ };
28
+ export type UsersProxy = {
29
+ [username: string]: UserEndpointMethods;
30
+ };
31
+
32
+ function createUserProxy(
33
+ username: string,
34
+ config: RequestConfig = {}
35
+ ): UserEndpointMethods {
36
+ return Object.fromEntries(
37
+ (Object.keys(kUserEndpointResponseMap) as UserEndpoint[]).map(
38
+ (endpoint) => [endpoint, () => new ApiEndpoint(`/users/${username}/${endpoint}`, config)]
39
+ )
40
+ ) as UserEndpointMethods;
41
+ }
42
+
43
+ export function createUsersProxy(config: RequestConfig = {}): UsersProxy {
44
+ return createApiProxy((username) => createUserProxy(username, config)) as UsersProxy;
45
+ }
46
+
47
+ export const users = createUsersProxy();
@@ -0,0 +1,105 @@
1
+ // Import Internal Dependencies
2
+ import { HttpLinkParser } from "./HttpLinkParser.ts";
3
+
4
+ // CONSTANTS
5
+ const kGithubURL = new URL("https://api.github.com/");
6
+
7
+ export class ApiEndpointOptions<T> {
8
+ /**
9
+ * By default, the raw response from the GitHub API is returned as-is.
10
+ * You can provide a custom extractor function to transform the raw response
11
+ * into an array of type T.
12
+ */
13
+ 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
+ }
25
+
26
+ export class ApiEndpoint<T> {
27
+ #userAgent: string;
28
+ #bearerToken?: string;
29
+
30
+ #nextURL: string | null = null;
31
+ #apiEndpoint: string | URL;
32
+ #extractor: (raw: any) => T[];
33
+
34
+ constructor(
35
+ apiEndpoint: string | URL,
36
+ options: ApiEndpointOptions<T> = {}
37
+ ) {
38
+ const {
39
+ userAgent = "@openally/github.sdk/1.0.0",
40
+ token,
41
+ extractor = ((raw) => raw as T[])
42
+ } = options;
43
+
44
+ this.#userAgent = userAgent;
45
+ this.#bearerToken = token;
46
+ this.#apiEndpoint = apiEndpoint;
47
+ this.#extractor = extractor;
48
+ }
49
+
50
+ setBearerToken(
51
+ token: string
52
+ ): this {
53
+ this.#bearerToken = token;
54
+
55
+ return this;
56
+ }
57
+
58
+ setAgent(
59
+ userAgent: string
60
+ ): this {
61
+ this.#userAgent = userAgent;
62
+
63
+ return this;
64
+ }
65
+
66
+ async #next(): Promise<T[]> {
67
+ const headers = {
68
+ "User-Agent": this.#userAgent,
69
+ Accept: "application/vnd.github.v3.raw",
70
+ ...(
71
+ typeof this.#bearerToken === "string" ?
72
+ { Authorization: `token ${this.#bearerToken}` } :
73
+ {}
74
+ )
75
+ };
76
+
77
+ const url = this.#nextURL === null ?
78
+ new URL(this.#apiEndpoint, kGithubURL) :
79
+ new URL(this.#nextURL, kGithubURL);
80
+ const response = await fetch(
81
+ url,
82
+ { headers }
83
+ );
84
+ const rawData = await response.json();
85
+
86
+ const linkHeader = response.headers.get("link");
87
+ this.#nextURL = linkHeader
88
+ ? HttpLinkParser.parse(linkHeader).get("next") ?? null
89
+ : null;
90
+
91
+ return this.#extractor(rawData);
92
+ }
93
+
94
+ async* iterate(): AsyncIterableIterator<T> {
95
+ do {
96
+ const pageResults = await this.#next();
97
+
98
+ yield* pageResults;
99
+ } while (this.#nextURL !== null);
100
+ }
101
+
102
+ all(): Promise<T[]> {
103
+ return Array.fromAsync(this.iterate());
104
+ }
105
+ }
@@ -0,0 +1,31 @@
1
+ // Import Internal Dependencies
2
+ import {
3
+ createUsersProxy,
4
+ type UsersProxy
5
+ } from "../api/users.ts";
6
+ import {
7
+ createReposProxy,
8
+ type ReposProxy
9
+ } from "../api/repos.ts";
10
+
11
+ export interface GithubClientOptions {
12
+ token?: string;
13
+ userAgent?: string;
14
+ }
15
+
16
+ export class GithubClient {
17
+ readonly users: UsersProxy;
18
+ readonly repos: ReposProxy;
19
+
20
+ constructor(
21
+ options: GithubClientOptions = {}
22
+ ) {
23
+ const config = {
24
+ token: options.token,
25
+ userAgent: options.userAgent
26
+ };
27
+
28
+ this.users = createUsersProxy(config);
29
+ this.repos = createReposProxy(config);
30
+ }
31
+ }
@@ -0,0 +1,17 @@
1
+ export class HttpLinkParser {
2
+ static parse(
3
+ headerValue: string
4
+ ): Map<string, string> {
5
+ const result = new Map<string, string>();
6
+
7
+ for (const part of headerValue.split(", ")) {
8
+ const urlMatch = part.match(/^<([^>]+)>/);
9
+ const relMatch = part.match(/rel="([^"]+)"/);
10
+ if (urlMatch && relMatch) {
11
+ result.set(relMatch[1], urlMatch[1]);
12
+ }
13
+ }
14
+
15
+ return result;
16
+ }
17
+ }
@@ -0,0 +1,9 @@
1
+ export function createApiProxy<T>(
2
+ factory: (key: string) => T
3
+ ): Record<string, T> {
4
+ return new Proxy(Object.create(null), {
5
+ get(_, key: string) {
6
+ return factory(key);
7
+ }
8
+ }) as Record<string, T>;
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./api/users.ts";
2
+ export * from "./api/repos.ts";
3
+ export * from "./class/GithubClient.ts";
4
+ export type { RequestConfig } from "./types.ts";
5
+
package/src/types.ts ADDED
@@ -0,0 +1,37 @@
1
+ // Import Third-party Dependencies
2
+ import type { Endpoints } from "@octokit/types";
3
+
4
+ export interface RequestConfig {
5
+ token?: string;
6
+ userAgent?: string;
7
+ }
8
+
9
+ // --- Repo entity types ---
10
+ export type Tag = Endpoints["GET /repos/{owner}/{repo}/tags"]["response"]["data"][number];
11
+ export type PullRequest = Endpoints["GET /repos/{owner}/{repo}/pulls"]["response"]["data"][number];
12
+ export type Issue = Endpoints["GET /repos/{owner}/{repo}/issues"]["response"]["data"][number];
13
+ export type Commit = Endpoints["GET /repos/{owner}/{repo}/commits"]["response"]["data"][number];
14
+ export type Workflow = Endpoints["GET /repos/{owner}/{repo}/actions/workflows"]["response"]["data"]["workflows"][number];
15
+ export type WorkflowRun = Endpoints[
16
+ "GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs"
17
+ ]["response"]["data"]["workflow_runs"][number];
18
+ export type Job = Endpoints["GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs"]["response"]["data"]["jobs"][number];
19
+ export type Artifact = Endpoints[
20
+ "GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts"
21
+ ]["response"]["data"]["artifacts"][number];
22
+
23
+ // Extractor response types (envelope objects returned by the GitHub API)
24
+ export type WorkflowsResponse = Endpoints["GET /repos/{owner}/{repo}/actions/workflows"]["response"]["data"];
25
+ export type WorkflowRunsResponse = Endpoints[
26
+ "GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs"
27
+ ]["response"]["data"];
28
+ export type JobsResponse = Endpoints["GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs"]["response"]["data"];
29
+ export type ArtifactsResponse = Endpoints["GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts"]["response"]["data"];
30
+
31
+ // --- User entity types ---
32
+ export type UserOrg = Endpoints["GET /users/{username}/orgs"]["response"]["data"][number];
33
+ export type UserRepo = Endpoints["GET /users/{username}/repos"]["response"]["data"][number];
34
+ export type UserGist = Endpoints["GET /users/{username}/gists"]["response"]["data"][number];
35
+ export type UserFollower = Endpoints["GET /users/{username}/followers"]["response"]["data"][number];
36
+ export type UserFollowing = Endpoints["GET /users/{username}/following"]["response"]["data"][number];
37
+ export type UserStarred = Endpoints["GET /users/{username}/starred"]["response"]["data"][number];