From 2f0badefcb72636cb8db1a926cfb7412a4175fa6 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 25 Nov 2025 12:18:40 +0530 Subject: [PATCH] Optimize organization queries with performance improvements and caching --- .../lib/server/queries/organisations/index.ts | 59 ++++++++--- .../queries/organisations/membershipCache.ts | 100 ++++++++++++++++++ 2 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 packages/lib/server/queries/organisations/membershipCache.ts diff --git a/packages/lib/server/queries/organisations/index.ts b/packages/lib/server/queries/organisations/index.ts index 4fab6453c93d56..aeb4d4f4aa847d 100644 --- a/packages/lib/server/queries/organisations/index.ts +++ b/packages/lib/server/queries/organisations/index.ts @@ -3,35 +3,66 @@ import { MembershipRole } from "@calcom/prisma/enums"; // export type OrganisationWithMembers = Awaited>; -// also returns team +/** + * Checks if a user is an admin or owner of an organization + * @param userId - User ID to check + * @param orgId - Organization/Team ID + * @returns boolean indicating admin status + */ export async function isOrganisationAdmin(userId: number, orgId: number) { - return ( - (await prisma.membership.findFirst({ - where: { - userId, - teamId: orgId, - OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }], - }, - })) || false - ); + const membership = await prisma.membership.findFirst({ + where: { + userId, + teamId: orgId, + OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }], + }, + select: { + id: true, + role: true, + }, + }); + + return !!membership; } +/** + * Checks if a user is an owner of an organization + * @param userId - User ID to check + * @param orgId - Organization/Team ID + * @returns boolean indicating owner status + */ export async function isOrganisationOwner(userId: number, orgId: number) { - return !!(await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId, teamId: orgId, role: MembershipRole.OWNER, }, - })); + select: { + id: true, + }, + }); + + return !!membership; } +/** + * Checks if a user is a member of an organization + * @param userId - User ID to check + * @param orgId - Organization/Team ID + * @returns boolean indicating membership status + */ export async function isOrganisationMember(userId: number, orgId: number) { - return !!(await prisma.membership.findUnique({ + const membership = await prisma.membership.findUnique({ where: { userId_teamId: { userId, teamId: orgId, }, }, - })); + select: { + id: true, + }, + }); + + return !!membership; } diff --git a/packages/lib/server/queries/organisations/membershipCache.ts b/packages/lib/server/queries/organisations/membershipCache.ts new file mode 100644 index 00000000000000..494363620f3537 --- /dev/null +++ b/packages/lib/server/queries/organisations/membershipCache.ts @@ -0,0 +1,100 @@ +import type { MembershipRole } from "@calcom/prisma/enums"; + +/** + * In-memory cache for organization membership queries + * This helps reduce database load for frequently accessed membership data + */ + +interface MembershipCacheEntry { + userId: number; + teamId: number; + role: MembershipRole; + timestamp: number; +} + +const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const membershipCache = new Map(); + +/** + * Generates cache key for membership lookup + */ +function getCacheKey(userId: number, teamId: number): string { + return `${userId}:${teamId}`; +} + +/** + * Checks if cache entry is still valid + */ +function isCacheValid(entry: MembershipCacheEntry): boolean { + return Date.now() - entry.timestamp <= CACHE_TTL_MS; +} + +/** + * Gets cached membership data if available and valid + */ +export function getCachedMembership(userId: number, teamId: number): MembershipCacheEntry | null { + const key = getCacheKey(userId, teamId); + const entry = membershipCache.get(key); + + if (!entry) { + return null; + } + + if (!isCacheValid(entry)) { + membershipCache.delete(key); + return null; + } + + return entry; +} + +/** + * Caches membership data + */ +export function setCachedMembership( + userId: number, + teamId: number, + role: MembershipRole +): void { + const key = getCacheKey(userId, teamId); + membershipCache.set(key, { + userId, + teamId, + role, + timestamp: Date.now(), + }); +} + +/** + * Invalidates cached membership for a user-team pair + */ +export function invalidateMembershipCache(userId: number, teamId: number): void { + const key = getCacheKey(userId, teamId); + membershipCache.delete(key); +} + +/** + * Clears all expired cache entries + */ +export function cleanupExpiredCache(): number { + let removedCount = 0; + + for (const [key, entry] of membershipCache.entries()) { + if (!isCacheValid(entry)) { + membershipCache.delete(key); + removedCount++; + } + } + + return removedCount; +} + +/** + * Gets current cache statistics + */ +export function getCacheStats() { + return { + size: membershipCache.size, + ttlMs: CACHE_TTL_MS, + }; +}