mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-25 23:00:32 +08:00
Instead of falling back to the default of 10 repos, set by radicle-httpd. We query the local httpd instance for the amount of repos hosted.
510 lines
10 KiB
TypeScript
510 lines
10 KiB
TypeScript
import { LRUCache } from "lru-cache";
|
|
import type { Commit, Commits } from "./project/commit.js";
|
|
import type { Embed } from "./project/comment.js";
|
|
import type { Fetcher, RequestOptions } from "./fetcher.js";
|
|
import type {
|
|
Issue,
|
|
IssueCreated,
|
|
IssueUpdateAction,
|
|
} from "./project/issue.js";
|
|
import type {
|
|
Patch,
|
|
PatchCreate,
|
|
PatchCreated,
|
|
PatchUpdateAction,
|
|
} from "./project/patch.js";
|
|
import type { SuccessResponse } from "./shared.js";
|
|
import type { ZodSchema } from "zod";
|
|
|
|
import { successResponseSchema } from "./shared.js";
|
|
import {
|
|
array,
|
|
boolean,
|
|
literal,
|
|
number,
|
|
optional,
|
|
record,
|
|
object,
|
|
string,
|
|
union,
|
|
z,
|
|
} from "zod";
|
|
|
|
import {
|
|
diffBlobSchema,
|
|
commitHeaderSchema,
|
|
commitSchema,
|
|
commitsSchema,
|
|
diffSchema,
|
|
} from "./project/commit.js";
|
|
|
|
import {
|
|
issueCreatedSchema,
|
|
issueSchema,
|
|
issuesSchema,
|
|
} from "./project/issue.js";
|
|
|
|
import {
|
|
patchSchema,
|
|
patchesSchema,
|
|
patchCreatedSchema,
|
|
} from "./project/patch.js";
|
|
|
|
const projectSchema = object({
|
|
id: string(),
|
|
name: string(),
|
|
description: string(),
|
|
defaultBranch: string(),
|
|
delegates: array(string()),
|
|
head: string(),
|
|
visibility: union([
|
|
object({ type: literal("public") }),
|
|
object({ type: literal("private"), allow: optional(array(string())) }),
|
|
]).optional(),
|
|
patches: object({
|
|
open: number(),
|
|
draft: number(),
|
|
archived: number(),
|
|
merged: number(),
|
|
}),
|
|
issues: object({
|
|
open: number(),
|
|
closed: number(),
|
|
}),
|
|
seeding: number(),
|
|
});
|
|
const projectsSchema = array(projectSchema);
|
|
|
|
export type Project = z.infer<typeof projectSchema>;
|
|
|
|
const activitySchema = object({
|
|
activity: array(number()),
|
|
});
|
|
|
|
export type Activity = z.infer<typeof activitySchema>;
|
|
|
|
const blobSchema = object({
|
|
binary: boolean(),
|
|
content: optional(string()),
|
|
name: string(),
|
|
path: string(),
|
|
lastCommit: commitHeaderSchema,
|
|
});
|
|
|
|
export type Blob = z.infer<typeof blobSchema>;
|
|
|
|
const treeEntrySchema = object({
|
|
path: string(),
|
|
name: string(),
|
|
oid: string(),
|
|
kind: union([literal("blob"), literal("tree"), literal("submodule")]),
|
|
});
|
|
|
|
export type TreeEntry = z.infer<typeof treeEntrySchema>;
|
|
|
|
export interface TreeStats {
|
|
commits: number;
|
|
branches: number;
|
|
contributors: number;
|
|
}
|
|
|
|
export type Tree = z.infer<typeof treeSchema>;
|
|
|
|
const treeSchema = object({
|
|
entries: array(treeEntrySchema),
|
|
lastCommit: commitHeaderSchema,
|
|
name: string(),
|
|
path: string(),
|
|
stats: object({
|
|
commits: number(),
|
|
branches: number(),
|
|
contributors: number(),
|
|
}),
|
|
});
|
|
|
|
export type Remote = z.infer<typeof remoteSchema>;
|
|
|
|
const remoteSchema = object({
|
|
id: string(),
|
|
alias: string().optional(),
|
|
heads: record(string(), string()),
|
|
delegate: boolean(),
|
|
});
|
|
|
|
const remotesSchema = array(remoteSchema) satisfies ZodSchema<Remote[]>;
|
|
|
|
export type DiffResponse = z.infer<typeof diffResponseSchema>;
|
|
|
|
const diffResponseSchema = object({
|
|
commits: array(commitHeaderSchema),
|
|
diff: diffSchema,
|
|
files: record(string(), diffBlobSchema),
|
|
});
|
|
|
|
export type ProjectListQuery = {
|
|
page?: number;
|
|
perPage?: number;
|
|
show?: "pinned" | "all";
|
|
};
|
|
export class Client {
|
|
#fetcher: Fetcher;
|
|
|
|
// We make the cache a static variable because clients are usually not
|
|
// persisted and new instances are frequently created.
|
|
static #cache = {
|
|
tree: new LRUCache<string, Tree>({ max: 300 }),
|
|
blob: new LRUCache<string, Blob>({ max: 500 }),
|
|
};
|
|
|
|
public constructor(fetcher: Fetcher) {
|
|
this.#fetcher = fetcher;
|
|
}
|
|
|
|
public async getByDelegate(
|
|
delegateId: string,
|
|
options?: RequestOptions,
|
|
): Promise<Project[]> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `delegates/${delegateId}/projects`,
|
|
options,
|
|
},
|
|
projectsSchema,
|
|
);
|
|
}
|
|
|
|
public async getById(id: string, options?: RequestOptions): Promise<Project> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}`,
|
|
options,
|
|
},
|
|
projectSchema,
|
|
);
|
|
}
|
|
|
|
public async getAll(
|
|
query?: ProjectListQuery,
|
|
options?: RequestOptions,
|
|
): Promise<Project[]> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: "projects",
|
|
query,
|
|
options,
|
|
},
|
|
projectsSchema,
|
|
);
|
|
}
|
|
|
|
public async getActivity(
|
|
id: string,
|
|
options?: RequestOptions,
|
|
): Promise<Activity> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/activity`,
|
|
options,
|
|
},
|
|
activitySchema,
|
|
);
|
|
}
|
|
|
|
public async getReadme(
|
|
id: string,
|
|
sha: string,
|
|
options?: RequestOptions,
|
|
): Promise<Blob> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/readme/${sha}`,
|
|
options,
|
|
},
|
|
blobSchema,
|
|
);
|
|
}
|
|
|
|
public async getBlob(
|
|
id: string,
|
|
sha: string,
|
|
path: string,
|
|
options?: RequestOptions,
|
|
): Promise<Blob> {
|
|
const cacheKey = `blob:${sha}:${path}`;
|
|
const cachedBlob = Client.#cache.blob.get(cacheKey);
|
|
if (cachedBlob) {
|
|
return cachedBlob;
|
|
}
|
|
const blob = await this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/blob/${sha}/${path}`,
|
|
options,
|
|
},
|
|
blobSchema,
|
|
);
|
|
Client.#cache.blob.set(cacheKey, blob);
|
|
return blob;
|
|
}
|
|
|
|
public async getTree(
|
|
id: string,
|
|
sha: string,
|
|
path?: string,
|
|
options?: RequestOptions,
|
|
): Promise<Tree> {
|
|
const cacheKey = `tree:${sha}:${path}`;
|
|
const cachedTree = Client.#cache.tree.get(cacheKey);
|
|
if (cachedTree) {
|
|
return cachedTree;
|
|
}
|
|
const tree = await this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/tree/${sha}/${path ?? ""}`,
|
|
options,
|
|
},
|
|
treeSchema,
|
|
);
|
|
Client.#cache.tree.set(cacheKey, tree);
|
|
return tree;
|
|
}
|
|
|
|
public async getAllRemotes(
|
|
id: string,
|
|
options?: RequestOptions,
|
|
): Promise<Remote[]> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/remotes`,
|
|
options,
|
|
},
|
|
remotesSchema,
|
|
);
|
|
}
|
|
|
|
public async getRemoteByPeer(
|
|
id: string,
|
|
peer: string,
|
|
options?: RequestOptions,
|
|
): Promise<Remote> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/remotes/${peer}`,
|
|
options,
|
|
},
|
|
remoteSchema,
|
|
);
|
|
}
|
|
|
|
public async getAllCommits(
|
|
id: string,
|
|
query?: {
|
|
parent?: string;
|
|
since?: number;
|
|
until?: number;
|
|
page?: number;
|
|
perPage?: number;
|
|
},
|
|
options?: RequestOptions,
|
|
): Promise<Commits> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/commits`,
|
|
query,
|
|
options,
|
|
},
|
|
commitsSchema,
|
|
);
|
|
}
|
|
|
|
public async getCommitBySha(
|
|
id: string,
|
|
sha: string,
|
|
options?: RequestOptions,
|
|
): Promise<Commit> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/commits/${sha}`,
|
|
options,
|
|
},
|
|
commitSchema,
|
|
);
|
|
}
|
|
|
|
public async getDiff(
|
|
id: string,
|
|
revisionBase: string,
|
|
revisionOid: string,
|
|
options?: RequestOptions,
|
|
): Promise<DiffResponse> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/diff/${revisionBase}/${revisionOid}`,
|
|
options,
|
|
},
|
|
diffResponseSchema,
|
|
);
|
|
}
|
|
|
|
public async getIssueById(
|
|
id: string,
|
|
issueId: string,
|
|
options?: RequestOptions,
|
|
): Promise<Issue> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/issues/${issueId}`,
|
|
options,
|
|
},
|
|
issueSchema,
|
|
);
|
|
}
|
|
|
|
public async getAllIssues(
|
|
id: string,
|
|
query?: {
|
|
page?: number;
|
|
perPage?: number;
|
|
state?: string;
|
|
},
|
|
options?: RequestOptions,
|
|
): Promise<Issue[]> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/issues`,
|
|
query,
|
|
options,
|
|
},
|
|
issuesSchema,
|
|
);
|
|
}
|
|
|
|
public async createIssue(
|
|
id: string,
|
|
body: {
|
|
title: string;
|
|
description: string;
|
|
assignees: string[];
|
|
embeds: Embed[];
|
|
labels: string[];
|
|
},
|
|
authToken: string,
|
|
options?: RequestOptions,
|
|
): Promise<IssueCreated> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "POST",
|
|
path: `projects/${id}/issues`,
|
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
body,
|
|
options,
|
|
},
|
|
issueCreatedSchema,
|
|
);
|
|
}
|
|
|
|
public async updateIssue(
|
|
id: string,
|
|
issueId: string,
|
|
body: IssueUpdateAction,
|
|
authToken: string,
|
|
options?: RequestOptions,
|
|
): Promise<SuccessResponse> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "PATCH",
|
|
path: `projects/${id}/issues/${issueId}`,
|
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
body,
|
|
options,
|
|
},
|
|
successResponseSchema,
|
|
);
|
|
}
|
|
|
|
public async getPatchById(
|
|
id: string,
|
|
patchId: string,
|
|
options?: RequestOptions,
|
|
): Promise<Patch> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/patches/${patchId}`,
|
|
options,
|
|
},
|
|
patchSchema,
|
|
);
|
|
}
|
|
|
|
public async getAllPatches(
|
|
id: string,
|
|
query?: {
|
|
page?: number;
|
|
perPage?: number;
|
|
state?: string;
|
|
},
|
|
options?: RequestOptions,
|
|
): Promise<Patch[]> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "GET",
|
|
path: `projects/${id}/patches`,
|
|
query,
|
|
options,
|
|
},
|
|
patchesSchema,
|
|
);
|
|
}
|
|
|
|
public async createPatch(
|
|
id: string,
|
|
body: PatchCreate,
|
|
authToken: string,
|
|
options?: RequestOptions,
|
|
): Promise<PatchCreated> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "POST",
|
|
path: `projects/${id}/patches`,
|
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
body,
|
|
options,
|
|
},
|
|
patchCreatedSchema,
|
|
);
|
|
}
|
|
|
|
public async updatePatch(
|
|
id: string,
|
|
patchId: string,
|
|
body: PatchUpdateAction,
|
|
authToken: string,
|
|
options?: RequestOptions,
|
|
): Promise<SuccessResponse> {
|
|
return this.#fetcher.fetchOk(
|
|
{
|
|
method: "PATCH",
|
|
path: `projects/${id}/patches/${patchId}`,
|
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
body,
|
|
options,
|
|
},
|
|
successResponseSchema,
|
|
);
|
|
}
|
|
}
|