Custom providers
The base class
import { Provider } from "@agntn/forges";
import type { ProviderRawTypes } from "@agntn/forges";
Provider owns the nine resource accessors. Its constructor binds repos, issues, pullRequests and the rest to protected methods you implement, and does the argument checks that are the same everywhere: an empty search query, a repository scope without an owner, two assignees where the platform takes one.
A concrete class brings its raw response types and the mappers:
interface MyRawTypes extends ProviderRawTypes {
owner: MyRawOwner;
repository: MyRawRepository;
issue: MyRawIssue;
pullRequest: MyRawPullRequest;
user: MyRawUser;
thread: MyRawThread;
comment: MyRawComment;
}
export class MyProvider extends Provider<MyRawTypes> {
constructor(config: ProviderConfig) {
super();
// create an HTTP client with the platform's auth header
}
protected mapRepository(raw: MyRawRepository): Repository {
return {
id: String(raw.id),
name: raw.name,
fullName: raw.full_name,
// …
};
}
protected async getRepo(owner: string, repo: string): Promise<Repository> {
try {
const data = await this.client<MyRawRepository>(`/repos/${owner}/${repo}`);
return this.mapRepository(data);
} catch (error) {
throw normalizeError(error, "my-platform");
}
}
// listRepos, listIssues, getIssue, createIssue, listPullRequests, getPullRequest,
// createPullRequest, listIssueComments, listPullRequestComments, getIssueComment,
// getPullRequestComment, getUser, getAuthenticatedUser, getCommit,
// listThreads, getThread, replyToThread, resolveThread, unresolveThread
}
Mappers are pure: raw in, normalized out, nothing else. IDs are strings, always String(raw.id), because one platform sends numbers and the day you compare 123 === "123" is a bad day.
What you may skip
Contribution templates, code search, CI runs, commit lists, issue and pull request search, pull request files and pull request checks all have defaults that reject with a ForgesError of status 501 and a sentence naming the operation. Implement what your platform has. The rest fails honestly instead of pretending.
The building blocks
import {
createHttpClient,
rawFetch,
normalizeError,
parseLinkHeader,
paginate,
fetchAllPages,
cachedFetch,
} from "@agntn/forges";
createHttpClient({ baseURL, token, tokenHeader, tokenPrefix })is the ofetch wrapper with auth, retry and rate limit handling the shipped providers use.rawFetchreturns the headers too, which a list needs for pagination.parseLinkHeaderreads aLinkheader intonext,prev,last.cachedFetchcaches a GET and refuses anything else.normalizeError(error, platform)maps a transport error onto theForgesErrorhierarchy. Every operation ends with it. No raw throws, ever.
Sub path imports
import { GitHubProvider } from "@agntn/forges/github";
import { GitLabProvider } from "@agntn/forges/gitlab";
import { GiteaProvider } from "@agntn/forges/gitea";
import { Provider } from "@agntn/forges/provider";
import type { Repository } from "@agntn/forges/types";
const gitea = new GiteaProvider({
token: process.env.GITEA_TOKEN,
baseURL: "https://codeberg.org",
});
console.log(gitea instanceof Provider); // true
One provider without the other two, better for tree shaking. Constructing a class directly skips the token detection of createProvider, so pass the token yourself.