Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,25 @@ import type { ICalendarSubscriptionPort } from "@calcom/features/calendar-subscr

export type CalendarSubscriptionProvider = "google_calendar" | "office365_calendar";

/**
* Generic calendar suffixes that should be excluded from subscription.
* These are special calendars (holidays, contacts, shared, imported, resources)
* that are not user's personal calendars and shouldn't be subscribed to for sync.
*/
export const GENERIC_CALENDAR_SUFFIXES: Record<CalendarSubscriptionProvider, string[]> = {
google_calendar: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
],
office365_calendar: [],
};

export interface AdapterFactory {
get(provider: CalendarSubscriptionProvider): ICalendarSubscriptionPort;
getProviders(): CalendarSubscriptionProvider[];
getGenericCalendarSuffixes(): string[];
}

/**
Expand Down Expand Up @@ -41,4 +57,16 @@ export class DefaultAdapterFactory implements AdapterFactory {
const providers: CalendarSubscriptionProvider[] = ["google_calendar"];
return providers;
}

/**
* Returns all generic calendar suffixes that should be excluded from subscription
* across all supported providers.
*
* @returns
*/
getGenericCalendarSuffixes(): string[] {
return Object.keys(GENERIC_CALENDAR_SUFFIXES).flatMap(
(provider) => GENERIC_CALENDAR_SUFFIXES[provider as CalendarSubscriptionProvider]
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export class CalendarSubscriptionService {
take: 100,
integrations: this.deps.adapterFactory.getProviders(),
teamIds,
genericCalendarSuffixes: this.deps.adapterFactory.getGenericCalendarSuffixes(),
});
log.debug("checkForNewSubscriptions", { count: rows.length });
await Promise.allSettled(rows.map(({ id }) => this.subscribe(id)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ describe("CalendarSubscriptionService", () => {
mockAdapterFactory = {
get: vi.fn().mockReturnValue(mockAdapter),
getProviders: vi.fn().mockReturnValue(["google_calendar", "office365_calendar"]),
getGenericCalendarSuffixes: vi.fn().mockReturnValue([
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
]),
};

mockSelectedCalendarRepository = {
Expand Down Expand Up @@ -385,6 +391,12 @@ describe("CalendarSubscriptionService", () => {
take: 100,
integrations: ["google_calendar", "office365_calendar"],
teamIds: [1, 2, 3],
genericCalendarSuffixes: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
],
});
expect(subscribeSpy).toHaveBeenCalledWith(mockSelectedCalendar.id);
});
Expand All @@ -411,6 +423,12 @@ describe("CalendarSubscriptionService", () => {
take: 100,
integrations: ["google_calendar", "office365_calendar"],
teamIds: [10, 20],
genericCalendarSuffixes: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
],
});
expect(subscribeSpy).toHaveBeenCalledTimes(2);
expect(subscribeSpy).toHaveBeenCalledWith("calendar-with-cache");
Expand All @@ -437,6 +455,12 @@ describe("CalendarSubscriptionService", () => {
take: 100,
integrations: ["google_calendar", "office365_calendar"],
teamIds: [teamId],
genericCalendarSuffixes: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
],
});
expect(mockSelectedCalendarRepository.findNextSubscriptionBatch).not.toHaveBeenCalledWith(
expect.objectContaining({
Expand All @@ -463,6 +487,12 @@ describe("CalendarSubscriptionService", () => {
take: 100,
integrations: ["google_calendar", "office365_calendar"],
teamIds: [],
genericCalendarSuffixes: [
"@group.v.calendar.google.com",
"@group.calendar.google.com",
"@import.calendar.google.com",
"@resource.calendar.google.com",
],
});
expect(subscribeSpy).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ export interface ISelectedCalendarRepository {
*
* @param take the number of calendars to take
* @param integrations the list of integrations
* @param genericCalendarSuffixes the list of generic calendar suffixes to exclude
*/
findNextSubscriptionBatch({
take,
teamIds,
integrations,
genericCalendarSuffixes,
}: {
take: number;
teamIds: number[];
integrations?: string[];
genericCalendarSuffixes?: string[];
}): Promise<SelectedCalendar[]>;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe("SelectedCalendarRepository", () => {
},
},
},
AND: undefined,
},
take: 10,
});
Expand Down Expand Up @@ -160,6 +161,7 @@ describe("SelectedCalendarRepository", () => {
},
},
},
AND: undefined,
},
take: 5,
});
Expand Down Expand Up @@ -189,6 +191,43 @@ describe("SelectedCalendarRepository", () => {
},
},
},
AND: undefined,
},
take: 10,
});

expect(result).toEqual(mockCalendars);
});

test("should filter out generic calendars when genericCalendarSuffixes is provided", async () => {
const mockCalendars = [mockSelectedCalendar];
vi.mocked(mockPrismaClient.selectedCalendar.findMany).mockResolvedValue(mockCalendars);

const genericSuffixes = ["@group.v.calendar.google.com", "@group.calendar.google.com"];

const result = await repository.findNextSubscriptionBatch({
take: 10,
teamIds: [1, 2],
integrations: ["google_calendar"],
genericCalendarSuffixes: genericSuffixes,
});

expect(mockPrismaClient.selectedCalendar.findMany).toHaveBeenCalledWith({
where: {
integration: { in: ["google_calendar"] },
OR: [{ syncSubscribedAt: null }, { channelExpiration: { lte: expect.any(Date) } }],
user: {
teams: {
some: {
teamId: { in: [1, 2] },
accepted: true,
},
},
},
AND: [
{ NOT: { externalId: { endsWith: "@group.v.calendar.google.com" } } },
{ NOT: { externalId: { endsWith: "@group.calendar.google.com" } } },
],
},
take: 10,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ISelectedCalendarRepository } from "@calcom/features/selectedCalen
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";

export class SelectedCalendarRepository implements ISelectedCalendarRepository {
export class PrismaSelectedCalendarRepository implements ISelectedCalendarRepository {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Repository filename/class mismatch 📘 Rule violation ✓ Correctness

• The repository file is named SelectedCalendarRepository.ts but now exports
  PrismaSelectedCalendarRepository, violating the required Prisma<Entity>Repository filename/class
  convention.
• This reduces consistency/discoverability and can confuse imports and code search for the canonical
  repository implementation.
Agent prompt
## Issue description
`PrismaSelectedCalendarRepository` is exported from `SelectedCalendarRepository.ts`, but repositories must follow the `Prisma<Entity>Repository.ts` naming convention with filename matching the exported class name.

## Issue Context
This violates the repository naming convention and can reduce discoverability and consistency across the codebase.

## Fix Focus Areas
- packages/features/selectedCalendar/repositories/SelectedCalendarRepository.ts[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

constructor(private prismaClient: PrismaClient) {}

async findById(id: string) {
Expand All @@ -19,10 +19,12 @@ export class SelectedCalendarRepository implements ISelectedCalendarRepository {
take,
teamIds,
integrations,
genericCalendarSuffixes,
}: {
take: number;
teamIds: number[];
integrations: string[];
genericCalendarSuffixes?: string[];
}) {
return this.prismaClient.selectedCalendar.findMany({
where: {
Comment on lines 19 to 30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Integrations optional mismatch 🐞 Bug ⛯ Reliability

ISelectedCalendarRepository.findNextSubscriptionBatch declares integrations as optional, but
  the Prisma implementation requires it and uses it unguarded.
• If any caller omits integrations (allowed by the interface), Prisma will receive in: undefined
  and likely throw at runtime.
• This is also a contract mismatch between interface and implementation that invites future bugs.
Agent prompt
### Issue description
`integrations` is optional in `ISelectedCalendarRepository.findNextSubscriptionBatch`, but the Prisma implementation requires it and uses it directly in `integration: { in: integrations }`. If a caller omits `integrations` (which the interface allows), this can cause a Prisma validation/runtime error.

### Issue Context
This PR adds another optional filter (`genericCalendarSuffixes`) and handles it defensively, making the lack of defensive handling for `integrations` more inconsistent and risky.

### Fix Focus Areas
- packages/features/selectedCalendar/repositories/SelectedCalendarRepository.interface.ts[26-36]
- packages/features/selectedCalendar/repositories/SelectedCalendarRepository.ts[18-33]

### Suggested fix options
- Option A: Make `integrations` required in the interface (and update any callers/tests accordingly).
- Option B (more flexible): Keep it optional and update implementation:
  - Use `integration: integrations?.length ? { in: integrations } : undefined` (or conditionally spread the filter) so Prisma doesn't receive `in: undefined`.
  - Decide expected behavior when `integrations` is omitted (e.g., return all integrations vs. return none).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Expand All @@ -36,6 +38,11 @@ export class SelectedCalendarRepository implements ISelectedCalendarRepository {
},
},
},
AND: genericCalendarSuffixes?.length
? genericCalendarSuffixes?.map((suffix) => ({
NOT: { externalId: { endsWith: suffix } },
}))
: undefined,
},
take,
});
Expand Down