import type { PrismaClient, PrincipalType } from "@prisma/client"; export type ExternalPrincipalType = Extract< PrincipalType, "FEISHU_CHAT" | "FEISHU_DEPARTMENT" | "FEISHU_USER_GROUP" >; export interface ExternalPrincipalMembershipInput { readonly feishuOpenId: string; readonly displayName: string; readonly principalType: ExternalPrincipalType; readonly principalId: string; } export interface SyncExternalPrincipalMembershipsInput { readonly source: string; readonly replaceSource?: boolean | undefined; readonly memberships: readonly ExternalPrincipalMembershipInput[]; } export interface SyncExternalPrincipalMembershipsResult { readonly synced: number; readonly revoked: number; } export async function syncExternalPrincipalMemberships( prisma: PrismaClient, input: SyncExternalPrincipalMembershipsInput, ): Promise { const syncedAt = new Date(); const incomingKeys = new Set(); let synced = 0; for (const item of input.memberships) { assertExternalPrincipalType(item.principalType); const user = await prisma.user.upsert({ where: { feishuOpenId: item.feishuOpenId }, update: { displayName: item.displayName }, create: { feishuOpenId: item.feishuOpenId, displayName: item.displayName, platformRoles: { create: { role: "TEACHER" } }, }, }); incomingKeys.add(externalMembershipKey(user.id, item.principalType, item.principalId)); const existing = await prisma.externalPrincipalMembership.findFirst({ where: { userId: user.id, principalType: item.principalType, principalId: item.principalId, source: input.source, revokedAt: null, }, select: { id: true }, }); if (existing === null) { await prisma.externalPrincipalMembership.create({ data: { userId: user.id, principalType: item.principalType, principalId: item.principalId, source: input.source, syncedAt, }, }); } else { await prisma.externalPrincipalMembership.update({ where: { id: existing.id }, data: { syncedAt }, }); } synced++; } let revoked = 0; if (input.replaceSource === true) { const active = await prisma.externalPrincipalMembership.findMany({ where: { source: input.source, revokedAt: null }, select: { id: true, userId: true, principalType: true, principalId: true }, }); const revokeIds = active .filter((membership) => !incomingKeys.has(externalMembershipKey( membership.userId, membership.principalType, membership.principalId, ))) .map((membership) => membership.id); if (revokeIds.length > 0) { const result = await prisma.externalPrincipalMembership.updateMany({ where: { id: { in: revokeIds } }, data: { revokedAt: syncedAt }, }); revoked = result.count; } } return { synced, revoked }; } function externalMembershipKey(userId: string, type: PrincipalType, id: string): string { return `${userId}:${type}:${id}`; } function assertExternalPrincipalType(type: PrincipalType): asserts type is ExternalPrincipalType { if (type !== "FEISHU_CHAT" && type !== "FEISHU_DEPARTMENT" && type !== "FEISHU_USER_GROUP") { throw new Error(`unsupported external principal type: ${type}`); } }