pr_01m47d24b0e6n91zwymwxg0vpx/services/repos/src/registry.ts

108 lines3,021 bytesCodeBlame
1import type { Repo, RepoPath, Viewer } from "@g1t/contracts";
2
3type RepoRow = {
4 id: string;
5 namespace: string;
6 name: string;
7 description: string | null;
8 is_private: number;
9 owner_id: string;
10 default_branch: string;
11 fork_of: string | null;
12 created_at: number;
13};
14
15function toRepo(row: RepoRow): Repo {
16 return {
17 id: row.id,
18 namespace: row.namespace,
19 name: row.name,
20 description: row.description,
21 isPrivate: row.is_private === 1,
22 ownerId: row.owner_id,
23 defaultBranch: row.default_branch,
24 forkOf: row.fork_of,
25 createdAt: row.created_at * 1000,
26 };
27}
28
29/** The key a repo is stored under in the git store. */
30export function storeKey(repo: RepoPath): string {
31 return `${repo.namespace}--${repo.name}`;
32}
33
34export function canRead(repo: Repo, viewer: Viewer): boolean {
35 return !repo.isPrivate || repo.ownerId === viewer?.id;
36}
37
38export function canWrite(repo: Repo, viewer: Viewer): boolean {
39 return repo.ownerId === viewer?.id;
40}
41
42/** Repository metadata in D1. */
43export class RepoRegistry {
44 constructor(private readonly db: D1Database) {}
45
46 async byPath(path: RepoPath): Promise<Repo | null> {
47 const row = await this.db
48 .prepare("SELECT * FROM repos WHERE namespace = ? AND name = ?")
49 .bind(path.namespace.toLowerCase(), path.name.toLowerCase())
50 .first<RepoRow>();
51 return row ? toRepo(row) : null;
52 }
53
54 async byId(id: string): Promise<Repo | null> {
55 const row = await this.db
56 .prepare("SELECT * FROM repos WHERE id = ?")
57 .bind(id)
58 .first<RepoRow>();
59 return row ? toRepo(row) : null;
60 }
61
62 /** Excludes attempt forks. */
63 async list(
64 viewer: Viewer,
65 options: { query?: string; namespace?: string },
66 ): Promise<Repo[]> {
67 const where = ["fork_of IS NULL", "(is_private = 0 OR owner_id = ?)"];
68 const params: unknown[] = [viewer?.id ?? ""];
69 if (options.namespace) {
70 where.push("namespace = ?");
71 params.push(options.namespace.toLowerCase());
72 }
73 if (options.query?.trim()) {
74 where.push("(name LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\')");
75 const pattern = `%${options.query.trim().replace(/[\\%_]/g, "\\$&")}%`;
76 params.push(pattern, pattern);
77 }
78 const { results } = await this.db
79 .prepare(
80 `SELECT * FROM repos WHERE ${where.join(" AND ")}
81 ORDER BY created_at DESC, id DESC LIMIT 50`,
82 )
83 .bind(...params)
84 .all<RepoRow>();
85 return results.map(toRepo);
86 }
87
88 async insert(repo: Repo): Promise<void> {
89 await this.db
90 .prepare(
91 `INSERT INTO repos
92 (id, namespace, name, description, is_private, owner_id, default_branch, fork_of, created_at)
93 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
94 )
95 .bind(
96 repo.id,
97 repo.namespace,
98 repo.name,
99 repo.description,
100 Number(repo.isPrivate),
101 repo.ownerId,
102 repo.defaultBranch,
103 repo.forkOf,
104 Math.floor(repo.createdAt / 1000),
105 )
106 .run();
107 }
108}