import { backfillAccounts, backfillDone } from "./backfill"; import { all } from "./db"; import { accountDb } from "./scoped-db"; import { locatedByPrefix, registerLocator } from "./scoped-db/locators"; const INVITER_INDEX = "org-invitation-inviters"; function inviterKey(inviterId: string, invitationId: string): string { return `${inviterId}:${invitationId}`; } /** Indexes an invitation under who sent it, so clearing that user's invitations needn't visit every org. */ export async function registerInviter(inviterId: string, invitationId: string, orgId: string): Promise { await registerLocator("org_invitation_by", inviterKey(inviterId, invitationId), orgId); } /** Cron step until done: indexes invitations sent before `org_invitation_by` locators existed. */ export function backfillInviters(): Promise { return backfillAccounts(INVITER_INDEX, async (accountId) => { const rows = await accountDb(accountId).all<{ id: string; org_id: string; invited_by: string }>( "SELECT id, org_id, invited_by FROM org_invitations WHERE invited_by IS NOT NULL", ); for (const row of rows) await registerInviter(row.invited_by, row.id, row.org_id); }); } /** * Organizations that may hold invitations to or from `userId`: those in the invitee's own copies, those it sent * from, and `memberOrgIds`. Every org until the inviter index is backfilled. */ export async function invitationOrgs(userId: string, memberOrgIds: string[]): Promise { const [received, sent] = await Promise.all([ accountDb(userId).all<{ org_id: string }>("SELECT DISTINCT org_id FROM org_invitations WHERE user_id = ?", userId), backfillDone(INVITER_INDEX).then((indexed) => indexed ? locatedByPrefix("org_invitation_by", `${userId}:`) : all<{ id: string }>("SELECT id FROM accounts WHERE kind = 'org'").then((rows) => rows.map((row) => row.id)), ), ]); return [...new Set([...memberOrgIds, ...received.map((row) => row.org_id), ...sent])].filter((id) => id !== userId); }