import { all, placeholders } from "./db";
import { badRequest } from "./errors";
import type { RepoRow } from "~/lib/types";
/**
* Who may be assigned to issues or asked for review: the owning user, collaborators and org members who can
* read the repository. Anyone else would be spammed with notifications and shown on content they have no
* relation to, so the filter runs server-side regardless of what the picker offered.
*/
export async function assignableUserIds(
repo: Pick<RepoRow, "id" | "owner_id">,
userIds: string[],
): Promise<Set<string>> {
if (userIds.length === 0) return new Set();
const rows = await all<{ id: string }>(
`SELECT a.id FROM accounts a JOIN users u ON u.account_id = a.id
WHERE a.id IN (${placeholders(userIds.length)}) AND (
a.id = ?${userIds.length + 1}
OR EXISTS (SELECT 1 FROM collaborators c WHERE c.repo_id = ?${userIds.length + 2} AND c.user_id = a.id)
OR EXISTS (SELECT 1 FROM org_members m JOIN accounts o ON o.id = m.org_id
WHERE m.org_id = ?${userIds.length + 1} AND m.user_id = a.id AND (m.role = 'owner' OR o.org_default_permission != 'none')))`,
...userIds,
repo.owner_id,
repo.id,
);
return new Set(rows.map((row) => row.id));
}
/** Rejects the request when any of `userIds` is not assignable, naming the offending logins. */
export async function assertAssignable(
repo: Pick<RepoRow, "id" | "owner_id">,
users: Array<{ id: string; name: string }>,
): Promise<void> {
const allowed = await assignableUserIds(
repo,
users.map((user) => user.id),
);
const rejected = users.filter((user) => !allowed.has(user.id)).map((user) => user.name);
if (rejected.length > 0) {
throw badRequest(