49 lines · 2.1 KB
import { all, placeholders } from "./db";
import { repoReadableSql } from "./repos";
import { userInbox } from "./user-inbox";
import type { InvolvementQuery } from "./involvement";
import type { Viewer } from "~/lib/types";

export type InvolvedItem = { number: number; title: string; is_pull: number; repo_name: string; owner_name: string };

/** Over-fetch so access checks that drop a few repos still fill the list. */
const OVERFETCH = 2;

/**
 * The viewer's involvement index, resolved against the repository directory: current owner and repo names,
 * and only repositories the viewer can still read (access may have changed since an entry was published).
 */
export async function involvedItems(viewer: Viewer, query: InvolvementQuery, limit: number): Promise<InvolvedItem[]> {
  const entries = await userInbox(viewer.id).listInvolvement({ ...query, limit: limit * OVERFETCH });
  const repoIds = [...new Set(entries.map((entry) => entry.repoId))];
  if (repoIds.length === 0) return [];
  const readable = viewer.isAdmin ? "(?1 = ?1)" : repoReadableSql("r", "?1");
  const repos = await all<{ id: string; name: string; owner_name: string }>(
    `SELECT r.id, r.name, a.name AS owner_name FROM repos r JOIN accounts a ON a.id = r.owner_id
     WHERE r.id IN (${placeholders(repoIds.length)}) AND r.hidden_at IS NULL AND a.disabled_at IS NULL AND ${readable}`,
    viewer.id,
    ...repoIds,
  );
  const byId = new Map(repos.map((repo) => [repo.id, repo]));
  return entries
    .flatMap((entry) => {
      const repo = byId.get(entry.repoId);
      if (!repo) return [];
      return [
        {
          number: entry.number,
          title: entry.title,
          is_pull: entry.isPull ? 1 : 0,
          repo_name: repo.name,
          owner_name: repo.owner_name,
        },
      ];