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 class/file name mismatch 📘 Rule violation ✓ Correctness

packages/features/selectedCalendar/repositories/SelectedCalendarRepository.ts exports
  PrismaSelectedCalendarRepository, but the filename is not PrismaSelectedCalendarRepository.ts
  (and does not follow the required Prisma<Entity>Repository.ts pattern).
• This violates the repository naming convention requirement and reduces
  discoverability/consistency, and can also break imports expecting an exported class named to match
  the file.
Agent prompt
## Issue description
The repository file `packages/features/selectedCalendar/repositories/SelectedCalendarRepository.ts` exports `PrismaSelectedCalendarRepository`, but the filename does not match the exported class name and does not follow the required `Prisma<Entity>Repository.ts` naming convention.

## Issue Context
Compliance requires repository files to be named `Prisma<Entity>Repository.ts` and export the matching PascalCase class. The current mismatch can also break existing imports that expect `SelectedCalendarRepository` to be exported from this path.

## Fix Focus Areas
- packages/features/selectedCalendar/repositories/SelectedCalendarRepository.ts[1-6]
- packages/features/selectedCalendar/repositories/SelectedCalendarRepository.test.ts[1-5]
- apps/web/app/api/cron/calendar-subscriptions/route.ts[4-13]
- apps/web/app/api/webhooks/calendar-subscription/[provider]/route.ts[5-16]

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

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. Broken repository export 🐞 Bug ✓ Correctness

SelectedCalendarRepository.ts now exports PrismaSelectedCalendarRepository, but existing code
  still imports/instantiates SelectedCalendarRepository from the same module path.
• This will cause a TypeScript compile error (missing named export) and block runtime execution for
  the affected Next routes and unit tests.
• Impact is high because the broken imports occur in production API routes (cron + webhook
  handlers).
Agent prompt
## Issue description
`packages/features/selectedCalendar/repositories/SelectedCalendarRepository.ts` now exports `PrismaSelectedCalendarRepository`, but several consumers still import/instantiate `SelectedCalendarRepository` from the same module path. This breaks compilation.

## Issue Context
This repository is used by calendar-subscription cron and webhook routes; compilation failure will block those endpoints.

## Fix Focus Areas
- packages/features/selectedCalendar/repositories/SelectedCalendarRepository.ts[1-10]
- apps/web/app/api/cron/calendar-subscriptions/route.ts[4-45]
- apps/web/app/api/webhooks/calendar-subscription/[provider]/route.ts[12-65]
- packages/features/selectedCalendar/repositories/SelectedCalendarRepository.test.ts[1-55]

## Suggested approach
Option A (compat/backwards-safe):
- Keep the class name `PrismaSelectedCalendarRepository`.
- Add `export { PrismaSelectedCalendarRepository as SelectedCalendarRepository };` (or re-add a wrapper class) so existing imports keep working.

Option B (explicit rename):
- Update all imports/usages to `PrismaSelectedCalendarRepository` and ensure no code still imports `SelectedCalendarRepository`.

ⓘ 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: {
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