pr_01m47d15m3e54sn21z27rpy5n9/services/repos/src/registry.ts
| 1 | import type { Repo, RepoPath, Viewer } from "@g1t/contracts"; |
| 2 | |
| 3 | type 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 | |
| 15 | function 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. */ |
| 30 | export function storeKey(repo: RepoPath): string { |
| 31 | return `${repo.namespace}--${repo.name}`; |
| 32 | } |
| 33 | |
| 34 | export function canRead(repo: Repo, viewer: Viewer): boolean { |
| 35 | return !repo.isPrivate || repo.ownerId === viewer?.id; |
| 36 | } |
| 37 | |
| 38 | export function canWrite(repo: Repo, viewer: Viewer): boolean { |
| 39 | return repo.ownerId === viewer?.id; |
| 40 | } |
| 41 | |
| 42 | /** Repository metadata in D1. */ |
| 43 | export 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 | } |