Skip to content

Optimize organization membership queries#13

Open
ShashankFC wants to merge 1 commit intomainfrom
feature/organization-query-performance
Open

Optimize organization membership queries#13
ShashankFC wants to merge 1 commit intomainfrom
feature/organization-query-performance

Conversation

@ShashankFC
Copy link
Collaborator

@ShashankFC ShashankFC commented Nov 25, 2025

This PR optimizes organization membership queries by adding selective field fetching, improved documentation, and an in-memory caching layer to reduce database load for frequently accessed membership data.


EntelligenceAI PR Summary

Optimizes organization membership permission checks through query refactoring and introduces an in-memory caching layer to reduce database load.

  • Refactored three permission check functions (isOrganisationAdmin, isOrganisationOwner, isOrganisationMember) to select only necessary fields
  • Added comprehensive JSDoc documentation to all permission check functions
  • Implemented consistent double-negation (!!) for boolean conversion across all functions
  • Introduced Map-based membership cache with 5-minute TTL keyed by userId:teamId pairs
  • Added cache utility functions for cleanup, invalidation, and statistics monitoring
  • Cache stores userId, teamId, role (MembershipRole enum), and timestamp for each entry

@entelligence-ai-pr-reviews
Copy link

Entelligence AI Vulnerability Scanner

Status: No security vulnerabilities found

Your code passed our comprehensive security analysis.

Analyzed 2 files in total

@entelligence-ai-pr-reviews
Copy link

Review Summary

❌ Rejected Comments (1)

This section lists 1 comments that were identified as fundamentally incorrect and filtered out during review validation. It is only visible on our internal repositories.

packages/lib/server/queries/organisations/membershipCache.ts (1)

82-87: cleanupExpiredCache iterates and deletes entries from membershipCache during iteration, which can cause skipped entries and incomplete cleanup in some JS engines.

📊 Impact Scores:

  • Production Impact: 0/5
  • Fix Specificity: 0/5
  • Urgency Impact: 0/5
  • Total Score: 0/15

Reason for rejection: The comment is based on incorrect understanding of JavaScript Map behavior. JavaScript Maps are specifically designed to handle deletions during iteration safely, unlike objects or arrays. The ECMAScript specification guarantees that Map iterators handle modifications correctly during iteration. The current code is already correct and safe.

Analysis: This is a false positive based on misconceptions about JavaScript Map iteration behavior. JavaScript Maps explicitly support safe deletion during iteration, making the suggested refactoring unnecessary overhead. The comment should be removed to avoid wasting developer time on a non-existent issue.


🏷️ Draft Comments (1)

Skipped posting 1 draft comments that were valid but scored below your review threshold (>=13/15). Feel free to update them here.

packages/lib/server/queries/organisations/index.ts (1)

12-67: isOrganisationAdmin, isOrganisationOwner, and isOrganisationMember each perform a separate database query for the same user/org pair, causing redundant DB load if called together (e.g., for permission checks). This can significantly degrade performance under high load or in batch operations.

📊 Impact Scores:

  • Production Impact: 3/5
  • Fix Specificity: 4/5
  • Urgency Impact: 2/5
  • Total Score: 9/15

🤖 AI Agent Prompt (Copy & Paste Ready):

Refactor packages/lib/server/queries/organisations/index.ts, lines 12-67. Problem: The functions isOrganisationAdmin, isOrganisationOwner, and isOrganisationMember each perform a separate database query for the same user/org pair, causing redundant DB load and measurable performance degradation when called together. Solution: Replace these with a single function (e.g., getOrganisationMembershipStatus) that fetches the membership record once and returns all three statuses (isMember, isAdmin, isOwner). Update all callers to use this new function instead of making three separate DB calls.

@entelligence-ai-pr-reviews
Copy link

🔬 Multi-Approach Review Summary

This PR was reviewed by 2 different approaches for comparison:

  • 🟢 Standard Reviewer: 0 comments
  • 🟠 LangGraph v3: 2 comments

Total: 2 review comments

Each comment is labeled with its source approach. This allows you to compare different AI review strategies.

🔒 Security Scan: Run once and shared across all approaches for efficiency.

Walkthrough

This PR optimizes organization membership permission checks by introducing an in-memory caching layer and refactoring existing query functions. The refactoring improves database query performance by selecting only necessary fields instead of full membership objects, while adding comprehensive JSDoc documentation for better code maintainability. A new caching mechanism with 5-minute TTL reduces database load for repeated membership lookups, featuring automatic expiration, cache invalidation, and monitoring capabilities. All permission check functions now use consistent boolean conversion patterns, maintaining existing functionality while significantly improving performance and code clarity.

Changes

File(s) Summary
packages/lib/server/queries/organisations/index.ts Refactored isOrganisationAdmin, isOrganisationOwner, and isOrganisationMember functions to use explicit select clauses for fetching only required fields (id and role), added comprehensive JSDoc documentation, and applied consistent double-negation boolean conversion.
packages/lib/server/queries/organisations/membershipCache.ts Introduced Map-based in-memory caching layer for organization membership queries with 5-minute TTL, including cache get/set/invalidate functions, automatic expiration checking, cleanup utilities, and cache statistics monitoring.

Sequence Diagram

This diagram shows the interactions between components:

sequenceDiagram
    participant Caller
    participant OrgService as Organization Service
    participant Prisma as Prisma ORM
    participant DB as Database

    Note over OrgService: Three membership check functions<br/>with optimized queries

    rect rgb(200, 220, 255)
        Note over Caller,DB: isOrganisationAdmin(userId, orgId)
        Caller->>OrgService: isOrganisationAdmin(userId, orgId)
        activate OrgService
        OrgService->>Prisma: findFirst(where: userId, teamId, role: ADMIN|OWNER)
        Note over Prisma: select: { id, role }
        Prisma->>DB: Query membership table
        DB-->>Prisma: membership record or null
        Prisma-->>OrgService: membership
        OrgService->>OrgService: Convert to boolean (!!membership)
        OrgService-->>Caller: boolean (admin status)
        deactivate OrgService
    end

    rect rgb(220, 255, 220)
        Note over Caller,DB: isOrganisationOwner(userId, orgId)
        Caller->>OrgService: isOrganisationOwner(userId, orgId)
        activate OrgService
        OrgService->>Prisma: findFirst(where: userId, teamId, role: OWNER)
        Note over Prisma: select: { id }
        Prisma->>DB: Query membership table
        DB-->>Prisma: membership record or null
        Prisma-->>OrgService: membership
        OrgService->>OrgService: Convert to boolean (!!membership)
        OrgService-->>Caller: boolean (owner status)
        deactivate OrgService
    end

    rect rgb(255, 240, 220)
        Note over Caller,DB: isOrganisationMember(userId, orgId)
        Caller->>OrgService: isOrganisationMember(userId, orgId)
        activate OrgService
        OrgService->>Prisma: findUnique(where: userId_teamId composite key)
        Note over Prisma: select: { id }
        Prisma->>DB: Query membership table
        DB-->>Prisma: membership record or null
        Prisma-->>OrgService: membership
        OrgService->>OrgService: Convert to boolean (!!membership)
        OrgService-->>Caller: boolean (member status)
        deactivate OrgService
    end

    Note over OrgService,Prisma: All queries now use select<br/>to fetch only required fields
Loading

🔒 Security Analysis

  • Vulnerabilities: 0
  • Bugs: 0
  • Code Smells: 1
  • Security Hotspots: 0

▶️AI Code Reviews for VS Code, Cursor, Windsurf
Install the extension

Note for Windsurf Please change the default marketplace provider to the following in the windsurf settings:

Marketplace Extension Gallery Service URL: https://marketplace.visualstudio.com/_apis/public/gallery

Marketplace Gallery Item URL: https://marketplace.visualstudio.com/items

Entelligence.ai can learn from your feedback. Simply add 👍 / 👎 emojis to teach it your preferences. More shortcuts below

Emoji Descriptions:

  • ⚠️ Potential Issue - May require further investigation.
  • 🔒 Security Vulnerability - Fix to ensure system safety.
  • 💻 Code Improvement - Suggestions to enhance code quality.
  • 🔨 Refactor Suggestion - Recommendations for restructuring code.
  • ℹ️ Others - General comments and information.

Interact with the Bot:

  • Send a message or request using the format:
    @entelligenceai + *your message*
Example: @entelligenceai Can you suggest improvements for this code?
  • Help the Bot learn by providing feedback on its responses.
    @entelligenceai + *feedback*
Example: @entelligenceai Do not comment on `save_auth` function !

Also you can trigger various commands with the bot by doing
@entelligenceai command

The current supported commands are

  1. config - shows the current config
  2. retrigger_review - retriggers the review

More commands to be added soon.

Comment on lines 3 to 68

// export type OrganisationWithMembers = Awaited<ReturnType<typeof getOrganizationMembers>>;

// 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;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🟠 [LangGraph v3] The async functions isOrganisationAdmin, isOrganisationOwner, and isOrganisationMember lack error handling. Without try/catch blocks, any errors during the database query will be unhandled, potentially causing the application to crash or behave unpredictably. Wrap the async operations in try/catch to handle potential errors gracefully.

📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
// export type OrganisationWithMembers = Awaited<ReturnType<typeof getOrganizationMembers>>;
// 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;
}
import { MembershipRole } from "@calcom/prisma/enums";
// export type OrganisationWithMembers = Awaited<ReturnType<typeof getOrganizationMembers>>;
/**
* 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) {
try {
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId: orgId,
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
},
select: {
id: true,
role: true,
},
});
return !!membership;
} catch (error) {
console.error('Error checking admin status:', error);
return false;
}
}
/**
* 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) {
try {
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId: orgId,
role: MembershipRole.OWNER,
},
select: {
id: true,
},
});
return !!membership;
} catch (error) {
console.error('Error checking owner status:', error);
return false;
}
}
/**
* 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) {
try {
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId: orgId,
},
},
select: {
id: true,
},
});
return !!membership;
} catch (error) {
console.error('Error checking membership status:', error);
return false;
}
}

Comment on lines +1 to +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<string, MembershipCacheEntry>();

/**
* 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,
};
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🟠 [LangGraph v3] The getCacheStats function should have an explicit return type for better type safety and clarity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant