122 lines · 5.0 KB
import { waitUntil } from "cloudflare:workers";
import { all, chunks, first, placeholders } from "./db";
import { accountDb } from "./scoped-db";
import { repoReadableSql } from "./repos";

/**
 * Notifications live in the recipient's AccountState; whether the recipient may still read a notification's
 * repository is a D1 directory question. These helpers apply `inboxReadableSql`'s rule in code: account-level
 * rows always show, repository rows only while the repository is visible to the recipient (site admins see all).
 */
export async function readableRepoIds(userId: string, repoIds: Iterable<string>): Promise<Set<string>> {
  const ids = [...new Set(repoIds)];
  if (ids.length === 0) return new Set();
  const admin = await first<{ ok: number }>("SELECT 1 AS ok FROM users WHERE account_id = ? AND is_admin = 1", userId);
  const out = new Set<string>();
  if (admin) {
    // Site admins read every repository that still exists.
    for (const part of chunks(ids)) {
      const rows = await all<{ id: string }>(
        `SELECT id FROM repos WHERE id IN (${placeholders(part.length)})`,
        ...part,
      );
      for (const row of rows) out.add(row.id);
    }
    return out;
  }
  for (const part of chunks(ids)) {
    const rows = await all<{ id: string }>(
      `SELECT r.id FROM repos r WHERE r.id IN (${placeholders(part.length)}) AND r.hidden_at IS NULL
       AND ${repoReadableSql("r", "?")}`,
      ...part,
      ...Array<string>(repoReadableSqlUserSlots).fill(userId),
    );
    for (const row of rows) out.add(row.id);
  }
  return out;
}

/** `repoReadableSql(alias, "?")` binds the user once per mention. */
const repoReadableSqlUserSlots = (repoReadableSql("r", "?").match(/\?/g) ?? []).length;