import { all, chunks, first, placeholders, run } from "./db";
import { accountDb, repoDb, stmt } from "./scoped-db";
import { forgetLocator } from "./scoped-db/locators";
import { forEachScope } from "./account-scope";
import { repoReadableSql } from "./repos";
import { invalidateUnread } from "./user-inbox";
import { env } from "cloudflare:workers";
import { addCounter } from "./counters";
import { REPO_STARS } from "./counter-keys";
/**
* Stars, watches, thread subscriptions, assignments, review requests and notifications feed the dashboard and
* inbox; rows held by users who can no longer read the repository must go when access is revoked
* (collaborator removal, org membership removal, public -> private), otherwise private activity keeps
* streaming to them. Stars and watches are D1 edges; subscriptions, assignments and review requests live in
* each repository's RepoState, notifications in each recipient's AccountState.
*/
const CANNOT_READ = `NOT EXISTS (SELECT 1 FROM repos r WHERE r.id = t.repo_id AND ${repoReadableSql("r", "t.user_id")})
AND NOT EXISTS (SELECT 1 FROM users u_ WHERE u_.account_id = t.user_id AND u_.is_admin = 1)`;
/** Of `userIds`, those who can no longer read `repoId` (a deleted repository is unreadable to everyone). */
async function unreadableBy(repoId: string, userIds: string[]): Promise<string[]> {
const out: string[] = [];
for (const ids of chunks([...new Set(userIds)])) {
const rows = await all<{ user_id: string }>(
`SELECT t.user_id FROM (SELECT a.id AS user_id, ? AS repo_id FROM accounts a WHERE a.id IN (${placeholders(ids.length)})) t
WHERE ${CANNOT_READ}`,
repoId,
...ids,
);
out.push(...rows.map((row) => row.user_id));
}
return out;
}
const REPO_EDGES = ["thread_subscriptions", "issue_assignees", "review_requests"] as const;
async function purgeRepoEdges(repoId: string, userId: string | null): Promise<void> {
const db = repoDb(repoId);
const userFilter = userId === null ? "" : " AND x.user_id = ?";