Skip to content

Improve cache validation and expiry handling#5

Open
ShashankFC wants to merge 1 commit intomainfrom
feature/improve-cache-validation
Open

Improve cache validation and expiry handling#5
ShashankFC wants to merge 1 commit intomainfrom
feature/improve-cache-validation

Conversation

@ShashankFC
Copy link
Collaborator

@ShashankFC ShashankFC commented Nov 25, 2025

This PR enhances the cache validation system with better expiry time calculations and improved team feature checks. It also optimizes calendar sync filtering for better performance.


EntelligenceAI PR Summary

This PR fixes calendar cache expiry handling and corrects a critical team feature validation bug, while simplifying sync logic and improving code readability.

  • Extended cache duration from 30 to 31 days for accurate monthly representation
  • Changed cache expiry queries from gte to gt to properly handle exact timestamp matches
  • Fixed inverted teamId validation logic in getShouldServeCache method
  • Removed syncSubscribedAt requirement from calendar sync filtering, relying only on syncToken
  • Extracted query result variable in isOrganisationAdmin for better code clarity

- Update cache expiry time calculation for better coverage
- Refine cache validation logic for team features
- Optimize calendar sync filtering
- Enhance organization admin check with detailed membership info
@entelligence-ai-pr-reviews
Copy link

Entelligence AI Vulnerability Scanner

Status: No security vulnerabilities found

Your code passed our comprehensive security analysis.

Analyzed 4 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/features/calendar-cache/calendar-cache.repository.ts (1)

126-126: expiresAt filter in findUnique query changed from gte to gt, which can cause valid cache entries to be missed if expiresAt equals current time, leading to incorrect cache invalidation.

📊 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 assumptions about cache expiration logic. Using 'gt' instead of 'gte' for cache expiration is standard practice and intentional - it ensures that entries expiring at the exact current time are treated as expired. The theoretical edge case of missing entries that expire at the exact millisecond is negligible in practice and the current implementation is consistent with standard cache invalidation patterns used elsewhere in the same file (line 111).

Analysis: The current cache expiration logic using 'gt' is correct and follows standard practices. The suggested change to 'gte' would actually be less appropriate for cache invalidation, as it would include entries that expire at the exact current time, which should be considered expired.


🏷️ Draft Comments (3)

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

packages/features/calendar-cache/calendar-cache.repository.ts (1)

111-111: expiresAt filter in cache queries changed from gte to gt, which can skip valid cache entries if expiresAt equals current time, causing unnecessary cache misses.

📊 Impact Scores:

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

🤖 AI Agent Prompt (Copy & Paste Ready):

In packages/features/calendar-cache/calendar-cache.repository.ts, line 111, the cache query now uses `expiresAt: { gt: new Date(Date.now()) }`, which can skip valid cache entries that expire exactly at the current time. Change this to `expiresAt: { gte: new Date(Date.now()) }` to ensure cache entries expiring at the current time are still considered valid.

packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts (1)

78-79: getAvailability now only checks for syncToken (not syncSubscribedAt), so calendars with a syncToken but missing syncSubscribedAt will be treated as cached, possibly returning stale or invalid data.

📊 Impact Scores:

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

🤖 AI Agent Prompt (Copy & Paste Ready):

In packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts, lines 78-79, the logic for splitting `selectedCalendars` into `withSync` and `withoutSync` only checks for `syncToken`, but should require both `syncToken` and `syncSubscribedAt` to ensure cache validity. Please update these lines so that `withSync` includes only calendars with both `syncToken` and `syncSubscribedAt`, and `withoutSync` includes those missing either.

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

28-35: isOrganisationMember uses findUnique for membership lookup, but if the membership table is large and not indexed on (userId, teamId), this can cause slow queries at scale.

📊 Impact Scores:

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

🤖 AI Agent Prompt (Copy & Paste Ready):

Check the `membership` table in your database to ensure there is a composite unique index on `(userId, teamId)` to support efficient lookups for the `isOrganisationMember` function in `packages/lib/server/queries/organisations/index.ts` lines 28-35. If the index does not exist, add it via a migration. This will prevent slow queries as the table grows.

@entelligence-ai-pr-reviews
Copy link

🔬 Multi-Approach Review Summary

This PR was reviewed by 2 different approaches for comparison:

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

Total: 8 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 addresses calendar cache management and team feature validation logic. The primary changes include extending the cache duration from 30 to 31 days for accurate monthly representation, and refining cache expiry queries to use strict comparison operators to prevent edge cases with expired entries. A critical bug fix inverts the team ID validation logic in the cache serving method, ensuring feature checks occur when a team ID is present rather than absent. Additionally, the calendar synchronization filtering is simplified by removing the subscription timestamp requirement, relying solely on sync tokens. A minor refactoring improves code readability in the organization admin query function.

Changes

File(s) Summary
packages/features/calendar-cache/calendar-cache.repository.ts Increased cache duration constant from 30 to 31 days; changed expiry date query condition from gte to gt in userId-based and credentialId-based cache retrieval methods to ensure entries expiring at exact current timestamp are treated as expired.
packages/features/calendar-cache/lib/getShouldServeCache.ts Fixed critical logic bug by inverting the teamId check condition - now returns false when teamId is truthy instead of falsy; added non-null assertion operator when passing teamId to checkIfTeamHasFeature.
packages/features/calendar-subscription/lib/cache/CalendarCacheWrapper.ts Simplified calendar sync filtering logic by removing syncSubscribedAt condition; calendars now categorized based solely on presence of syncToken rather than requiring both syncToken and syncSubscribedAt.
packages/lib/server/queries/organisations/index.ts Refactored isOrganisationAdmin function by extracting Prisma query result into separate membership variable for improved readability; no functional logic changes.

Sequence Diagram

This diagram shows the interactions between components:

sequenceDiagram
    participant Client
    participant CalendarCacheRepository
    participant Database
    
    Note over CalendarCacheRepository: Configuration<br/>CACHING_TIME = 31 days (updated from 30)
    
    Client->>CalendarCacheRepository: getCachedData(userId, key)
    activate CalendarCacheRepository
    
    CalendarCacheRepository->>CalendarCacheRepository: parseKeyForCache(args)
    
    CalendarCacheRepository->>Database: findFirst({<br/>where: userId, key,<br/>expiresAt > now })
    Note over Database: Changed from gte to gt<br/>Entries expiring exactly now<br/>are now considered expired
    
    alt Cache entry found and valid
        Database-->>CalendarCacheRepository: Return cached entry
        CalendarCacheRepository-->>Client: Return cached data
    else No valid cache entry
        Database-->>CalendarCacheRepository: Return null
        CalendarCacheRepository-->>Client: Return null (cache miss)
    end
    
    deactivate CalendarCacheRepository
    
    Note over Client,Database: Similar flow for credentialId-based lookup
    
    Client->>CalendarCacheRepository: getCachedData(credentialId, key)
    activate CalendarCacheRepository
    
    CalendarCacheRepository->>Database: findUnique({<br/>where: credentialId, key,<br/>expiresAt > now })
    Note over Database: Also changed from gte to gt
    
    alt Cache entry found and valid
        Database-->>CalendarCacheRepository: Return cached entry
        CalendarCacheRepository-->>Client: Return cached data
    else No valid cache entry
        Database-->>CalendarCacheRepository: Return null
        CalendarCacheRepository-->>Client: Return null (cache miss)
    end
    
    deactivate CalendarCacheRepository
Loading

🔒 Security Analysis

  • Vulnerabilities: 0
  • Bugs: 0
  • Code Smells: 11
  • 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 +13 to +14
if (teamId) return false;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId!, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);

Choose a reason for hiding this comment

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

correctness: 🟢 [Standard Reviewer] if (teamId) return false; inverts the intended logic: it returns false when teamId is present, preventing feature checks for valid teams and always returning false for valid input.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/features/calendar-cache/lib/getShouldServeCache.ts, lines 13-14, the logic is inverted: `if (teamId) return false;` causes the function to return false when a valid teamId is provided, which is incorrect. Change it to `if (!teamId) return false;` and remove the non-null assertion on teamId in the next line. The corrected code should check for missing teamId and only then return false.
📝 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
if (teamId) return false;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId!, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);
if (!teamId) return false;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);

Comment on lines +8 to +15
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId: orgId,
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
},
});
return membership || false;

Choose a reason for hiding this comment

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

correctness: 🟢 [Standard Reviewer] isOrganisationAdmin now returns the membership object or false, which may break callers expecting a boolean result and cause logic errors in permission checks.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/lib/server/queries/organisations/index.ts, lines 8-15, the function `isOrganisationAdmin` now returns the membership object or `false`, which can break callers expecting a boolean and cause permission logic errors. Change the return statement to `return !!membership;` so the function always returns a boolean.
📝 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
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId: orgId,
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
},
});
return membership || false;
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId: orgId,
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
},
});
return !!membership;

Comment on lines 11 to 17
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });

const MS_PER_DAY = 24 * 60 * 60 * 1000;
const ONE_MONTH_IN_MS = 30 * MS_PER_DAY;
const ONE_MONTH_IN_MS = 31 * MS_PER_DAY;
const CACHING_TIME = ONE_MONTH_IN_MS;

function parseKeyForCache(args: FreeBusyArgs): string {

Choose a reason for hiding this comment

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

Correctness: 🟠 [LangGraph v3] The constant ONE_MONTH_IN_MS is set to 31 days, which may not accurately represent a month. Consider using an average month duration or a more precise calculation based on the current month.

📝 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
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const ONE_MONTH_IN_MS = 30 * MS_PER_DAY;
const ONE_MONTH_IN_MS = 31 * MS_PER_DAY;
const CACHING_TIME = ONE_MONTH_IN_MS;
function parseKeyForCache(args: FreeBusyArgs): string {
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const ONE_MONTH_IN_MS = 30.44 * MS_PER_DAY; // Average month duration
const CACHING_TIME = ONE_MONTH_IN_MS;
function parseKeyForCache(args: FreeBusyArgs): string {

Comment on lines 108 to 114
where: {
userId,
key,
expiresAt: { gte: new Date(Date.now()) },
expiresAt: { gt: new Date(Date.now()) },
},
orderBy: {
// In case of multiple entries for same key and userId, we prefer the one with highest expiry, which will be the most updated one

Choose a reason for hiding this comment

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

Correctness: 🟠 [LangGraph v3] The change from gte to gt for expiresAt may exclude entries that expire exactly at the current time, potentially causing valid cache entries to be missed. Verify if this change is intentional.

📝 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
where: {
userId,
key,
expiresAt: { gte: new Date(Date.now()) },
expiresAt: { gt: new Date(Date.now()) },
},
orderBy: {
// In case of multiple entries for same key and userId, we prefer the one with highest expiry, which will be the most updated one
where: {
userId,
key,
expiresAt: { gte: new Date(Date.now()) },
},
orderBy: {
// In case of multiple entries for same key and userId, we prefer the one with highest expiry, which will be the most updated one
}

Comment on lines 123 to 129
credentialId,
key,
},
expiresAt: { gte: new Date(Date.now()) },
expiresAt: { gt: new Date(Date.now()) },
},
});
}

Choose a reason for hiding this comment

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

Correctness: 🟠 [LangGraph v3] Change from gte to gt in expiresAt ensures only future entries are retrieved. Confirm this aligns with business logic.

Comment on lines 10 to 16

async getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
if (typeof shouldServeCache === "boolean") return shouldServeCache;
if (!teamId) return false;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);
if (teamId) return false;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId!, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);
}
}

Choose a reason for hiding this comment

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

Correctness: 🟠 [LangGraph v3] The use of teamId! is unsafe as it assumes teamId is always defined, which can lead to runtime errors if teamId is undefined. Consider adding a check or handling the case where teamId might be undefined.

📝 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
async getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
if (typeof shouldServeCache === "boolean") return shouldServeCache;
if (!teamId) return false;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);
if (teamId) return false;
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId!, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);
}
}
constructor(private readonly dependencies: ICacheService) {}
async getShouldServeCache(shouldServeCache?: boolean | undefined, teamId?: number) {
if (typeof shouldServeCache === "boolean") return shouldServeCache;
if (teamId !== undefined) {
return await this.dependencies.featuresRepository.checkIfTeamHasFeature(teamId, CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE);
}
return false;
}

Comment on lines 75 to 82

if (!selectedCalendars?.length) return [];

const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
const withSync = selectedCalendars.filter((c) => c.syncToken);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken);

const results: EventBusyDate[] = [];

Choose a reason for hiding this comment

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

Correctness: 🟠 [LangGraph v3] Align the filtering logic in getAvailability with getAvailabilityWithTimeZones by checking both syncToken and syncSubscribedAt.

📝 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
if (!selectedCalendars?.length) return [];
const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
const withSync = selectedCalendars.filter((c) => c.syncToken);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken);
const results: EventBusyDate[] = [];
});
if (!selectedCalendars?.length) return [];
const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
const results: EventBusyDate[] = [];

Comment on lines 5 to 18

// also returns team
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 }],
},
});
return membership || false;
}
export async function isOrganisationOwner(userId: number, orgId: number) {
return !!(await prisma.membership.findFirst({

Choose a reason for hiding this comment

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

Correctness: 🟠 [LangGraph v3] The isOrganisationAdmin function lacks error handling for the async operation. Wrap the await call in a try/catch block to handle potential errors from the database query.

📝 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
// also returns team
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 }],
},
});
return membership || false;
}
export async function isOrganisationOwner(userId: number, orgId: number) {
return !!(await prisma.membership.findFirst({
// export type OrganisationWithMembers = Awaited<ReturnType<typeof getOrganizationMembers>>;
// also returns team
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 }],
},
});
return membership || false;
} catch (error) {
console.error('Error fetching membership:', error);
return false;
}
}
export async function isOrganisationOwner(userId: number, orgId: number) {
return !!(await prisma.membership.findFirst({
where: {
userId,
teamId: orgId,
role: MembershipRole.OWNER,
},
}));
}

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